mssql-auth 0.8.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
479
480
481
482
483
484
485
486
//! Windows SSPI authentication provider.
//!
//! This module provides Windows-native Security Support Provider Interface (SSPI)
//! authentication for SQL Server connections. It supports both Windows integrated
//! authentication (current user) and explicit Windows credentials.
//!
//! ## Features
//!
//! - **Negotiate protocol**: Automatically selects between Kerberos and NTLM
//! - **Integrated auth**: Use current Windows login credentials
//! - **Explicit credentials**: Supply username/password for different account
//! - **Cross-platform**: Uses sspi-rs which works on Windows and emulates SSPI on Unix
//!
//! ## Example
//!
//! ```rust,ignore
//! use mssql_auth::SspiAuth;
//!
//! // Use current Windows login (integrated auth)
//! let auth = SspiAuth::new("sqlserver.example.com", 1433)?;
//!
//! // Or with explicit credentials
//! let auth = SspiAuth::with_credentials(
//!     "sqlserver.example.com",
//!     1433,
//!     "DOMAIN\\username",
//!     "password",
//! )?;
//!
//! // Start authentication
//! let initial_token = auth.initialize()?;
//!
//! // Process server response
//! let response_token = auth.step(&server_token)?;
//! ```
//!
//! ## Wire Protocol
//!
//! SSPI tokens are exchanged using the TDS SSPI packet type (0x11).
//! The authentication follows the SPNEGO/Negotiate protocol:
//!
//! 1. Client sends Login7 packet with integrated auth flag
//! 2. Server responds with SSPI challenge token
//! 3. Client processes challenge, sends response token
//! 4. Server validates and completes authentication

use std::sync::Mutex;

use sspi::{
    AuthIdentity, BufferType, ClientRequestFlags, CredentialUse, Credentials, CredentialsBuffers,
    DataRepresentation, Negotiate, NegotiateConfig, SecurityBuffer, SecurityStatus, Sspi, SspiImpl,
    Username, ntlm::NtlmConfig,
};

use crate::error::AuthError;
use crate::provider::{AuthData, AuthMethod, AuthProvider};

/// Windows SSPI authentication provider.
///
/// This provider implements SSPI-based authentication for SQL Server,
/// supporting both integrated (current user) and explicit credential modes.
///
/// # Thread Safety
///
/// The SSPI context is wrapped in a Mutex for thread safety, though
/// authentication is typically single-threaded per connection.
pub struct SspiAuth {
    /// The target service principal name (e.g., "MSSQLSvc/host:port").
    spn: String,
    /// Optional explicit credentials (domain\user, password).
    credentials: Option<(String, String)>,
    /// The SSPI context state.
    context: Mutex<SspiContext>,
}

/// Internal SSPI context state.
struct SspiContext {
    /// The Negotiate SSP instance.
    negotiate: Negotiate,
    /// Acquired credentials handle.
    creds_handle: Option<CredentialsBuffers>,
    /// Whether authentication has completed.
    complete: bool,
}

/// Create a default Negotiate configuration using NTLM.
fn create_negotiate_config() -> NegotiateConfig {
    NegotiateConfig::new(
        Box::new(NtlmConfig::default()),
        // Allow Kerberos and NTLM, but not PKU2U
        Some("kerberos,ntlm".to_string()),
        // Client computer name (not critical for SQL Server auth)
        String::new(),
    )
}

impl SspiAuth {
    /// Create a new SSPI authentication provider for integrated auth.
    ///
    /// Uses the current Windows user's credentials.
    ///
    /// # Arguments
    ///
    /// * `hostname` - The SQL Server hostname
    /// * `port` - The SQL Server port (typically 1433)
    ///
    /// # Errors
    ///
    /// Returns an error if the Negotiate context cannot be created.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let auth = SspiAuth::new("sqlserver.contoso.com", 1433)?;
    /// ```
    pub fn new(hostname: &str, port: u16) -> Result<Self, AuthError> {
        // SQL Server SPN format: MSSQLSvc/hostname:port
        let spn = format!("MSSQLSvc/{hostname}:{port}");

        let negotiate = Negotiate::new_client(create_negotiate_config())
            .map_err(|e| AuthError::Sspi(format!("Failed to create Negotiate context: {e}")))?;

        Ok(Self {
            spn,
            credentials: None,
            context: Mutex::new(SspiContext {
                negotiate,
                creds_handle: None,
                complete: false,
            }),
        })
    }

    /// Create a new SSPI authentication provider with explicit credentials.
    ///
    /// Use this when authenticating as a different user than the current
    /// Windows login.
    ///
    /// # Arguments
    ///
    /// * `hostname` - The SQL Server hostname
    /// * `port` - The SQL Server port
    /// * `username` - Username in "DOMAIN\\user" or "user@domain" format
    /// * `password` - Password for the user
    ///
    /// # Errors
    ///
    /// Returns an error if the Negotiate context cannot be created.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let auth = SspiAuth::with_credentials(
    ///     "sqlserver.contoso.com",
    ///     1433,
    ///     "CONTOSO\\sqluser",
    ///     "MyP@ssw0rd",
    /// )?;
    /// ```
    pub fn with_credentials(
        hostname: &str,
        port: u16,
        username: impl Into<String>,
        password: impl Into<String>,
    ) -> Result<Self, AuthError> {
        let spn = format!("MSSQLSvc/{hostname}:{port}");

        let negotiate = Negotiate::new_client(create_negotiate_config())
            .map_err(|e| AuthError::Sspi(format!("Failed to create Negotiate context: {e}")))?;

        Ok(Self {
            spn,
            credentials: Some((username.into(), password.into())),
            context: Mutex::new(SspiContext {
                negotiate,
                creds_handle: None,
                complete: false,
            }),
        })
    }

    /// Create with a custom service principal name.
    ///
    /// Use when the SPN doesn't follow the standard format,
    /// such as when using a SQL Server alias or cluster name.
    ///
    /// # Arguments
    ///
    /// * `spn` - The full service principal name
    ///
    /// # Errors
    ///
    /// Returns an error if the Negotiate context cannot be created.
    pub fn with_spn(spn: impl Into<String>) -> Result<Self, AuthError> {
        let negotiate = Negotiate::new_client(create_negotiate_config())
            .map_err(|e| AuthError::Sspi(format!("Failed to create Negotiate context: {e}")))?;

        Ok(Self {
            spn: spn.into(),
            credentials: None,
            context: Mutex::new(SspiContext {
                negotiate,
                creds_handle: None,
                complete: false,
            }),
        })
    }

    /// Initialize the SSPI context and get the initial token.
    ///
    /// This must be called first to start the authentication handshake.
    /// The returned token should be sent to the server.
    ///
    /// # Errors
    ///
    /// Returns an error if credential acquisition or 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()))?;

        // Acquire credentials
        let credentials = if let Some((ref username, ref password)) = self.credentials {
            // Parse username into domain and user parts
            let parsed_user = Username::parse(username)
                .map_err(|e| AuthError::Sspi(format!("Invalid username format: {e}")))?;

            let identity = AuthIdentity {
                username: parsed_user,
                password: password.clone().into(),
            };

            // Convert to Credentials enum
            Some(Credentials::from(identity))
        } else {
            None
        };

        let creds_result = {
            let mut builder = ctx
                .negotiate
                .acquire_credentials_handle()
                .with_credential_use(CredentialUse::Outbound);

            // Only add auth data if we have explicit credentials
            if let Some(ref creds) = credentials {
                builder = builder.with_auth_data(creds);
            }

            builder
                .execute(&mut ctx.negotiate)
                .map_err(|e| AuthError::Sspi(format!("Failed to acquire credentials: {e}")))?
        };

        // Store credentials handle (may be None for integrated auth)
        ctx.creds_handle = creds_result.credentials_handle;

        // Initialize security context
        // Take credentials handle temporarily to avoid overlapping mutable borrows
        let mut creds = ctx.creds_handle.take();
        let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
        let spn = self.spn.clone();

        let mut builder = ctx
            .negotiate
            .initialize_security_context()
            .with_credentials_handle(&mut creds)
            .with_context_requirements(
                ClientRequestFlags::MUTUAL_AUTH
                    | ClientRequestFlags::REPLAY_DETECT
                    | ClientRequestFlags::SEQUENCE_DETECT,
            )
            .with_target_data_representation(DataRepresentation::Native)
            .with_target_name(&spn)
            .with_output(&mut output_buffer);

        let init_result = ctx
            .negotiate
            .initialize_security_context_impl(&mut builder)
            .map_err(|e| AuthError::Sspi(format!("Failed to initialize context: {e}")))?
            .resolve_to_result()
            .map_err(|e| AuthError::Sspi(format!("Failed to resolve context: {e}")))?;

        // Put credentials handle back
        ctx.creds_handle = creds;

        // Check result status
        match init_result.status {
            SecurityStatus::Ok | SecurityStatus::ContinueNeeded => {
                if init_result.status == SecurityStatus::Ok {
                    ctx.complete = true;
                }

                // Return the output token
                let token = output_buffer
                    .into_iter()
                    .find(|b| b.buffer_type.buffer_type == BufferType::Token)
                    .map(|b| b.buffer)
                    .unwrap_or_default();

                Ok(token)
            }
            status => Err(AuthError::Sspi(format!(
                "Unexpected status during initialization: {status:?}"
            ))),
        }
    }

    /// Process a server token and generate a response.
    ///
    /// Call this method each time the server sends an SSPI token.
    /// If the return value is `None`, authentication is complete.
    ///
    /// # Arguments
    ///
    /// * `server_token` - The SSPI token received from the server
    ///
    /// # Errors
    ///
    /// Returns an error if the context step fails or the context
    /// hasn't been initialized.
    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.creds_handle.is_none() {
            return Err(AuthError::Sspi(
                "Context not initialized - call initialize() first".into(),
            ));
        }

        // Set up input and output buffers
        let mut input_buffer = vec![SecurityBuffer::new(
            server_token.to_vec(),
            BufferType::Token,
        )];
        let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
        let spn = self.spn.clone();

        // Take credentials handle temporarily to avoid overlapping mutable borrows
        let mut creds = ctx.creds_handle.take();

        let mut builder = ctx
            .negotiate
            .initialize_security_context()
            .with_credentials_handle(&mut creds)
            .with_context_requirements(
                ClientRequestFlags::MUTUAL_AUTH
                    | ClientRequestFlags::REPLAY_DETECT
                    | ClientRequestFlags::SEQUENCE_DETECT,
            )
            .with_target_data_representation(DataRepresentation::Native)
            .with_target_name(&spn)
            .with_input(&mut input_buffer)
            .with_output(&mut output_buffer);

        let result = ctx
            .negotiate
            .initialize_security_context_impl(&mut builder)
            .map_err(|e| AuthError::Sspi(format!("SSPI step failed: {e}")))?
            .resolve_to_result()
            .map_err(|e| AuthError::Sspi(format!("Failed to resolve step result: {e}")))?;

        // Put credentials handle back
        ctx.creds_handle = creds;

        match result.status {
            SecurityStatus::Ok => {
                ctx.complete = true;
                // Return final token if there is one
                let token = output_buffer
                    .into_iter()
                    .find(|b| {
                        b.buffer_type.buffer_type == BufferType::Token && !b.buffer.is_empty()
                    })
                    .map(|b| b.buffer);
                Ok(token)
            }
            SecurityStatus::ContinueNeeded => {
                let token = output_buffer
                    .into_iter()
                    .find(|b| b.buffer_type.buffer_type == BufferType::Token)
                    .map(|b| b.buffer)
                    .unwrap_or_default();
                Ok(Some(token))
            }
            status => Err(AuthError::Sspi(format!(
                "Unexpected status during step: {status:?}"
            ))),
        }
    }

    /// 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 SspiAuth {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SspiAuth")
            .field("spn", &self.spn)
            .field("has_explicit_credentials", &self.credentials.is_some())
            .field("complete", &self.is_complete())
            .finish()
    }
}

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

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

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

impl AuthProvider for SspiAuth {
    fn method(&self) -> AuthMethod {
        AuthMethod::Integrated
    }

    fn authenticate(&self) -> Result<AuthData, AuthError> {
        // Generate initial SSPI blob
        let blob = self.initialize()?;
        Ok(AuthData::Sspi { blob })
    }
}

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

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

    #[test]
    fn test_custom_spn() {
        let auth = SspiAuth::with_spn("MSSQLSvc/cluster.example.com:1433").unwrap();
        assert_eq!(auth.spn(), "MSSQLSvc/cluster.example.com:1433");
    }

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

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

    #[test]
    fn test_with_credentials() {
        let auth = SspiAuth::with_credentials("test.example.com", 1433, "DOMAIN\\user", "password")
            .unwrap();
        let debug = format!("{auth:?}");
        assert!(debug.contains("has_explicit_credentials: true"));
    }
}