use super::auth_extractors::{
AuthExtractionResult, AuthExtractor, AuthUtils, TransportAuthContext, TransportAuthError,
TransportRequest, TransportType,
};
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct WebSocketAuthConfig {
pub require_handshake_auth: bool,
pub allow_post_connect_auth: bool,
pub supported_methods: Vec<WebSocketAuthMethod>,
pub enable_per_message_auth: bool,
pub auth_subprotocol: Option<String>,
pub auth_timeout_secs: u64,
}
impl Default for WebSocketAuthConfig {
fn default() -> Self {
Self {
require_handshake_auth: true,
allow_post_connect_auth: true,
supported_methods: vec![
WebSocketAuthMethod::HandshakeHeaders,
WebSocketAuthMethod::QueryParams,
WebSocketAuthMethod::FirstMessage,
],
enable_per_message_auth: false,
auth_subprotocol: Some("mcp-auth".to_string()),
auth_timeout_secs: 30,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebSocketAuthMethod {
HandshakeHeaders,
QueryParams,
FirstMessage,
Subprotocol,
PerMessage,
}
pub struct WebSocketAuthExtractor {
config: WebSocketAuthConfig,
}
impl WebSocketAuthExtractor {
pub fn new(config: WebSocketAuthConfig) -> Self {
Self { config }
}
pub fn default() -> Self {
Self::new(WebSocketAuthConfig::default())
}
fn extract_handshake_headers(&self, headers: &HashMap<String, String>) -> AuthExtractionResult {
if !self
.config
.supported_methods
.contains(&WebSocketAuthMethod::HandshakeHeaders)
{
return Ok(None);
}
if let Some(auth_header) = headers
.get("Authorization")
.or_else(|| headers.get("authorization"))
{
if auth_header.starts_with("Bearer ") {
match AuthUtils::extract_bearer_token(auth_header) {
Ok(token) => {
AuthUtils::validate_api_key_format(&token)?;
let context = TransportAuthContext::new(
token,
"HandshakeHeaders".to_string(),
TransportType::WebSocket,
);
return Ok(Some(context));
}
Err(e) => return Err(e),
}
}
}
if let Some(api_key) = AuthUtils::extract_api_key_header(headers) {
AuthUtils::validate_api_key_format(&api_key)?;
let context = TransportAuthContext::new(
api_key,
"HandshakeHeaders".to_string(),
TransportType::WebSocket,
);
return Ok(Some(context));
}
if let Some(api_key) = headers.get("Sec-WebSocket-Protocol") {
if let Some(auth_token) = self.extract_from_subprotocol(api_key) {
AuthUtils::validate_api_key_format(&auth_token)?;
let context = TransportAuthContext::new(
auth_token,
"Subprotocol".to_string(),
TransportType::WebSocket,
);
return Ok(Some(context));
}
}
Ok(None)
}
fn extract_query_params(&self, request: &TransportRequest) -> AuthExtractionResult {
if !self
.config
.supported_methods
.contains(&WebSocketAuthMethod::QueryParams)
{
return Ok(None);
}
for param_name in &["api_key", "apikey", "key", "token", "access_token"] {
if let Some(api_key) = request.get_query_param(param_name) {
AuthUtils::validate_api_key_format(api_key)?;
let context = TransportAuthContext::new(
api_key.clone(),
"QueryParams".to_string(),
TransportType::WebSocket,
);
return Ok(Some(context));
}
}
Ok(None)
}
fn extract_first_message(&self, request: &TransportRequest) -> AuthExtractionResult {
if !self
.config
.supported_methods
.contains(&WebSocketAuthMethod::FirstMessage)
{
return Ok(None);
}
if let Some(body) = &request.body {
if let Some(auth_data) = self.find_auth_in_message(body) {
AuthUtils::validate_api_key_format(&auth_data)?;
let context = TransportAuthContext::new(
auth_data,
"FirstMessage".to_string(),
TransportType::WebSocket,
);
return Ok(Some(context));
}
}
Ok(None)
}
fn extract_from_subprotocol(&self, subprotocol: &str) -> Option<String> {
if let Some(auth_protocol) = &self.config.auth_subprotocol {
let prefix = format!("{}.", auth_protocol);
if let Some(token) = subprotocol.strip_prefix(&prefix) {
return Some(token.to_string());
}
let prefix_dash = format!("{}-", auth_protocol);
if let Some(token) = subprotocol.strip_prefix(&prefix_dash) {
return Some(token.to_string());
}
}
None
}
fn find_auth_in_message(&self, message: &Value) -> Option<String> {
if let Some(auth) = message.get("auth") {
if let Some(api_key) = auth.get("api_key").and_then(|v| v.as_str()) {
return Some(api_key.to_string());
}
if let Some(token) = auth.get("token").and_then(|v| v.as_str()) {
return Some(token.to_string());
}
}
if let Some(params) = message.get("params") {
if let Some(api_key) = params.get("api_key").and_then(|v| v.as_str()) {
return Some(api_key.to_string());
}
if let Some(client_info) = params.get("clientInfo") {
if let Some(auth) = client_info.get("authentication") {
if let Some(api_key) = auth.get("api_key").and_then(|v| v.as_str()) {
return Some(api_key.to_string());
}
}
}
}
if let Some(api_key) = message.get("api_key").and_then(|v| v.as_str()) {
return Some(api_key.to_string());
}
None
}
fn enrich_context(
&self,
mut context: TransportAuthContext,
request: &TransportRequest,
) -> TransportAuthContext {
if let Some(client_ip) = AuthUtils::extract_client_ip(&request.headers) {
context = context.with_client_ip(client_ip);
}
if let Some(user_agent) = AuthUtils::extract_user_agent(&request.headers) {
context = context.with_user_agent(user_agent);
}
if let Some(origin) = request.get_header("Origin") {
context = context.with_metadata("origin".to_string(), origin.clone());
}
if let Some(protocols) = request.get_header("Sec-WebSocket-Protocol") {
context = context.with_metadata("protocols".to_string(), protocols.clone());
}
if let Some(version) = request.get_header("Sec-WebSocket-Version") {
context = context.with_metadata("ws_version".to_string(), version.clone());
}
context
}
pub fn has_handshake_auth(&self, request: &TransportRequest) -> bool {
if request.headers.contains_key("Authorization")
|| AuthUtils::extract_api_key_header(&request.headers).is_some()
{
return true;
}
for param_name in &["api_key", "apikey", "key", "token", "access_token"] {
if request.query_params.contains_key(*param_name) {
return true;
}
}
if let Some(protocols) = request.get_header("Sec-WebSocket-Protocol") {
if let Some(auth_protocol) = &self.config.auth_subprotocol {
if protocols.contains(auth_protocol) {
return true;
}
}
}
false
}
}
#[async_trait]
impl AuthExtractor for WebSocketAuthExtractor {
async fn extract_auth(&self, request: &TransportRequest) -> AuthExtractionResult {
if let Ok(Some(context)) = self.extract_handshake_headers(&request.headers) {
return Ok(Some(self.enrich_context(context, request)));
}
if let Ok(Some(context)) = self.extract_query_params(request) {
return Ok(Some(self.enrich_context(context, request)));
}
if let Ok(Some(context)) = self.extract_first_message(request) {
return Ok(Some(self.enrich_context(context, request)));
}
if self.config.require_handshake_auth && !self.config.allow_post_connect_auth {
return Err(TransportAuthError::NoAuth);
}
Ok(None)
}
fn transport_type(&self) -> TransportType {
TransportType::WebSocket
}
fn can_handle(&self, request: &TransportRequest) -> bool {
request.headers.contains_key("Sec-WebSocket-Key")
|| request.headers.contains_key("Upgrade")
|| request.metadata.contains_key("websocket")
}
async fn validate_auth(
&self,
context: &TransportAuthContext,
) -> Result<(), TransportAuthError> {
if context.credential.is_empty() {
return Err(TransportAuthError::InvalidFormat(
"Empty credential".to_string(),
));
}
if context.method == "QueryParams" {
tracing::warn!(
"WebSocket authentication via query parameters is less secure - consider using headers"
);
}
Ok(())
}
}
impl WebSocketAuthConfig {
pub fn secure() -> Self {
Self {
require_handshake_auth: true,
allow_post_connect_auth: false,
supported_methods: vec![WebSocketAuthMethod::HandshakeHeaders],
enable_per_message_auth: false,
auth_subprotocol: Some("mcp-auth".to_string()),
auth_timeout_secs: 10,
}
}
pub fn flexible() -> Self {
Self {
require_handshake_auth: false,
allow_post_connect_auth: true,
supported_methods: vec![
WebSocketAuthMethod::HandshakeHeaders,
WebSocketAuthMethod::QueryParams,
WebSocketAuthMethod::FirstMessage,
],
enable_per_message_auth: false,
auth_subprotocol: Some("mcp-auth".to_string()),
auth_timeout_secs: 30,
}
}
pub fn development() -> Self {
Self {
require_handshake_auth: false,
allow_post_connect_auth: true,
supported_methods: vec![
WebSocketAuthMethod::HandshakeHeaders,
WebSocketAuthMethod::QueryParams,
WebSocketAuthMethod::FirstMessage,
WebSocketAuthMethod::Subprotocol,
],
enable_per_message_auth: false,
auth_subprotocol: Some("mcp-auth".to_string()),
auth_timeout_secs: 60,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_handshake_header_extraction() {
let extractor = WebSocketAuthExtractor::default();
let mut headers = HashMap::new();
headers.insert(
"Authorization".to_string(),
"Bearer lmcp_test_1234567890abcdef".to_string(),
);
headers.insert("Sec-WebSocket-Key".to_string(), "test-key".to_string());
let request = TransportRequest::from_headers(headers);
let result = tokio_test::block_on(extractor.extract_auth(&request)).unwrap();
assert!(result.is_some());
let context = result.unwrap();
assert_eq!(context.credential, "lmcp_test_1234567890abcdef");
assert_eq!(context.method, "HandshakeHeaders");
assert_eq!(context.transport_type, TransportType::WebSocket);
}
#[test]
fn test_query_parameter_extraction() {
let extractor = WebSocketAuthExtractor::default();
let request = TransportRequest::new()
.with_header("Sec-WebSocket-Key".to_string(), "test-key".to_string())
.with_query_param(
"api_key".to_string(),
"lmcp_test_1234567890abcdef".to_string(),
);
let result = tokio_test::block_on(extractor.extract_auth(&request)).unwrap();
assert!(result.is_some());
let context = result.unwrap();
assert_eq!(context.credential, "lmcp_test_1234567890abcdef");
assert_eq!(context.method, "QueryParams");
}
#[test]
fn test_first_message_extraction() {
let extractor = WebSocketAuthExtractor::default();
let auth_message = json!({
"auth": {
"api_key": "lmcp_test_1234567890abcdef"
}
});
let request = TransportRequest::new()
.with_header("Sec-WebSocket-Key".to_string(), "test-key".to_string())
.with_body(auth_message);
let result = tokio_test::block_on(extractor.extract_auth(&request)).unwrap();
assert!(result.is_some());
let context = result.unwrap();
assert_eq!(context.credential, "lmcp_test_1234567890abcdef");
assert_eq!(context.method, "FirstMessage");
}
#[test]
fn test_subprotocol_extraction() {
let extractor = WebSocketAuthExtractor::default();
let mut headers = HashMap::new();
headers.insert(
"Sec-WebSocket-Protocol".to_string(),
"mcp-auth.lmcp_test_1234567890abcdef".to_string(),
);
headers.insert("Sec-WebSocket-Key".to_string(), "test-key".to_string());
let request = TransportRequest::from_headers(headers);
let result = tokio_test::block_on(extractor.extract_auth(&request)).unwrap();
assert!(result.is_some());
let context = result.unwrap();
assert_eq!(context.credential, "lmcp_test_1234567890abcdef");
assert_eq!(context.method, "Subprotocol");
}
#[test]
fn test_mcp_initialize_message() {
let extractor = WebSocketAuthExtractor::default();
let init_message = json!({
"method": "initialize",
"params": {
"clientInfo": {
"name": "test-client",
"authentication": {
"api_key": "lmcp_test_1234567890abcdef"
}
}
}
});
let request = TransportRequest::new()
.with_header("Sec-WebSocket-Key".to_string(), "test-key".to_string())
.with_body(init_message);
let result = tokio_test::block_on(extractor.extract_auth(&request)).unwrap();
assert!(result.is_some());
let context = result.unwrap();
assert_eq!(context.credential, "lmcp_test_1234567890abcdef");
assert_eq!(context.method, "FirstMessage");
}
#[test]
fn test_has_handshake_auth() {
let extractor = WebSocketAuthExtractor::default();
let request1 = TransportRequest::new()
.with_header("Authorization".to_string(), "Bearer token123".to_string());
assert!(extractor.has_handshake_auth(&request1));
let request2 =
TransportRequest::new().with_query_param("api_key".to_string(), "token123".to_string());
assert!(extractor.has_handshake_auth(&request2));
let request3 = TransportRequest::new();
assert!(!extractor.has_handshake_auth(&request3));
}
#[test]
fn test_configuration_presets() {
let secure_config = WebSocketAuthConfig::secure();
assert!(secure_config.require_handshake_auth);
assert!(!secure_config.allow_post_connect_auth);
assert_eq!(secure_config.auth_timeout_secs, 10);
let flexible_config = WebSocketAuthConfig::flexible();
assert!(!flexible_config.require_handshake_auth);
assert!(flexible_config.allow_post_connect_auth);
let dev_config = WebSocketAuthConfig::development();
assert!(!dev_config.require_handshake_auth);
assert_eq!(dev_config.auth_timeout_secs, 60);
}
}