1use std::sync::Arc;
10
11use axum::http::header::AUTHORIZATION;
12use axum::http::request::Parts;
13use axum::http::{HeaderValue, StatusCode};
14use axum::response::{IntoResponse, Response};
15use base64::Engine as _;
16use tracing::{debug, warn};
17
18use super::{AuthMethod, AuthResult};
19use crate::state::AppState;
20
21#[derive(Clone)]
29pub struct NegotiateOutToken(pub HeaderValue);
30
31#[derive(Clone)]
36pub struct TlsChannelBinding(pub Vec<u8>);
37
38pub async fn try_extract_gssapi(
45 parts: &mut Parts,
46 app: &Arc<AppState>,
47) -> Option<Result<AuthResult, Response>> {
48 let auth_header = parts.headers.get(AUTHORIZATION)?.to_str().ok()?;
49
50 let token_b64 = auth_header.strip_prefix("Negotiate ")?;
52
53 let gss_cred = match app.gss_cred.as_ref() {
55 Some(cred) => Arc::clone(cred),
56 None => {
57 debug!("Negotiate header present but GSSAPI not configured");
58 return Some(Err((
59 StatusCode::UNAUTHORIZED,
60 "GSSAPI is not configured on this server",
61 )
62 .into_response()));
63 }
64 };
65
66 let token_bytes = match base64::engine::general_purpose::STANDARD.decode(token_b64) {
68 Ok(t) => t,
69 Err(_) => {
70 return Some(Err((
71 StatusCode::BAD_REQUEST,
72 "malformed Negotiate token encoding",
73 )
74 .into_response()));
75 }
76 };
77
78 const MAX_TOKEN_BYTES: usize = 128 * 1024;
80 if token_bytes.len() > MAX_TOKEN_BYTES {
81 return Some(Err((
82 StatusCode::BAD_REQUEST,
83 "Negotiate token exceeds size limit",
84 )
85 .into_response()));
86 }
87
88 let channel_binding: Option<Vec<u8>> = parts
90 .extensions
91 .get::<TlsChannelBinding>()
92 .map(|b| b.0.clone());
93
94 let binding_owned = channel_binding;
97 let token_owned = token_bytes;
98 let result = tokio::task::spawn_blocking(move || {
99 negotiate_accept(&gss_cred, &token_owned, binding_owned.as_deref())
100 })
101 .await;
102
103 let negotiate_result = match result {
104 Ok(r) => r,
105 Err(e) => {
106 tracing::error!(error = %e, "GSSAPI spawn_blocking panicked");
107 return Some(Err(StatusCode::INTERNAL_SERVER_ERROR.into_response()));
108 }
109 };
110
111 match negotiate_result {
112 Ok(NegotiateSuccess {
113 principal,
114 out_token,
115 }) => {
116 debug!(principal = %principal, "GSSAPI authentication succeeded");
117
118 if !out_token.is_empty() {
120 let b64 = base64::engine::general_purpose::STANDARD.encode(&out_token);
121 if let Ok(hv) = HeaderValue::from_str(&format!("Negotiate {b64}")) {
122 parts.extensions.insert(NegotiateOutToken(hv));
123 }
124 }
125
126 Some(Ok(AuthResult {
127 identity: principal,
128 method: AuthMethod::Gssapi,
129 client_cert_der: None,
130 subject_dn: None,
131 subject_alt_names: Vec::new(),
132 extended_key_usage: Vec::new(),
133 }))
134 }
135 Err(NegotiateError::Continue(out_token)) => {
136 let b64 = base64::engine::general_purpose::STANDARD.encode(&out_token);
138 let mut resp = (StatusCode::UNAUTHORIZED, "").into_response();
139 if let Ok(hv) = HeaderValue::from_str(&format!("Negotiate {b64}")) {
140 resp.headers_mut().insert("WWW-Authenticate", hv);
141 }
142 Some(Err(resp))
143 }
144 Err(NegotiateError::Failed(msg)) => {
145 warn!(error = %msg, "GSSAPI authentication failed");
146 Some(Err(
147 (StatusCode::FORBIDDEN, "GSSAPI authentication failed").into_response()
148 ))
149 }
150 }
151}
152
153pub fn negotiate_challenge() -> Response {
158 let mut resp = (StatusCode::UNAUTHORIZED, "").into_response();
159 resp.headers_mut()
160 .insert("WWW-Authenticate", HeaderValue::from_static("Negotiate"));
161 resp
162}
163
164struct NegotiateSuccess {
167 principal: String,
168 out_token: Vec<u8>,
169}
170
171#[allow(dead_code)]
172enum NegotiateError {
173 Continue(Vec<u8>),
175 Failed(String),
177}
178
179fn negotiate_accept(
186 _cred: &dyn std::any::Any,
187 token: &[u8],
188 channel_binding: Option<&[u8]>,
189) -> Result<NegotiateSuccess, NegotiateError> {
190 let _ = token;
200 let _ = channel_binding;
201
202 Err(NegotiateError::Failed(
203 "GSSAPI not yet implemented".to_string(),
204 ))
205}