Skip to main content

turbomcp_core/
auth.rs

1//! Authentication traits and types for MCP servers.
2//!
3//! This module provides platform-adaptive authentication primitives that work
4//! on both native and WASM targets. The traits use [`MaybeSend`] and [`MaybeSync`]
5//! bounds for portability.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────┐
11//! │                    turbomcp-core                         │
12//! │  ┌─────────────┐  ┌──────────────┐  ┌───────────────┐  │
13//! │  │ Credential  │  │ Principal    │  │ Authenticator │  │
14//! │  │ (enum)      │  │ (validated)  │  │ (trait)       │  │
15//! │  └─────────────┘  └──────────────┘  └───────────────┘  │
16//! └─────────────────────────────────────────────────────────┘
17//!                            │
18//!            ┌───────────────┴───────────────┐
19//!            ▼                               ▼
20//!    ┌───────────────┐               ┌───────────────┐
21//!    │ turbomcp-auth │               │ turbomcp-wasm │
22//!    │ (native impl) │               │ (WASM impl)   │
23//!    │               │               │               │
24//!    │ JwtValidator  │               │ WasmJwtAuth   │
25//!    │ (jsonwebtoken)│               │ (Web Crypto)  │
26//!    └───────────────┘               └───────────────┘
27//! ```
28//!
29//! # Example
30//!
31//! ```rust,ignore
32//! use turbomcp_core::auth::{Authenticator, Credential, Principal};
33//!
34//! // Implement for your platform
35//! struct MyAuthenticator;
36//!
37//! impl Authenticator for MyAuthenticator {
38//!     type Error = MyError;
39//!
40//!     async fn authenticate(&self, credential: &Credential) -> Result<Principal, Self::Error> {
41//!         // Validate credential and return principal
42//!     }
43//! }
44//! ```
45
46use crate::marker::{MaybeSend, MaybeSync};
47use alloc::collections::BTreeMap;
48use alloc::string::{String, ToString};
49use alloc::vec;
50use alloc::vec::Vec;
51use core::fmt;
52use core::future::Future;
53use serde::{Deserialize, Serialize};
54
55/// Credential types that can be extracted from requests.
56///
57/// This enum represents the various authentication credentials
58/// that MCP servers may accept.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Credential {
61    /// Bearer token (e.g., JWT, OAuth access token)
62    /// Extracted from: `Authorization: Bearer <token>`
63    Bearer(String),
64
65    /// API key
66    /// Extracted from: `X-API-Key: <key>` or `Authorization: ApiKey <key>`
67    ApiKey(String),
68
69    /// Basic authentication
70    /// Extracted from: `Authorization: Basic <base64(username:password)>`
71    Basic {
72        /// Username
73        username: String,
74        /// Password
75        password: String,
76    },
77
78    /// Custom credential scheme
79    /// For platform-specific auth (e.g., Cloudflare Access)
80    Custom {
81        /// Scheme name (e.g., "CF-Access")
82        scheme: String,
83        /// Credential value
84        value: String,
85    },
86}
87
88impl Credential {
89    /// Create a Bearer credential
90    pub fn bearer(token: impl Into<String>) -> Self {
91        Self::Bearer(token.into())
92    }
93
94    /// Create an API key credential
95    pub fn api_key(key: impl Into<String>) -> Self {
96        Self::ApiKey(key.into())
97    }
98
99    /// Create a Basic auth credential
100    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
101        Self::Basic {
102            username: username.into(),
103            password: password.into(),
104        }
105    }
106
107    /// Create a custom credential
108    pub fn custom(scheme: impl Into<String>, value: impl Into<String>) -> Self {
109        Self::Custom {
110            scheme: scheme.into(),
111            value: value.into(),
112        }
113    }
114
115    /// Returns true if this is a Bearer credential
116    pub fn is_bearer(&self) -> bool {
117        matches!(self, Self::Bearer(_))
118    }
119
120    /// Returns the bearer token if this is a Bearer credential
121    pub fn as_bearer(&self) -> Option<&str> {
122        match self {
123            Self::Bearer(token) => Some(token),
124            _ => None,
125        }
126    }
127}
128
129/// A validated principal (identity) after successful authentication.
130///
131/// This struct contains the authenticated identity and any claims
132/// extracted from the credential (e.g., JWT claims).
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct Principal {
135    /// Unique subject identifier (e.g., user ID, `sub` claim in JWT)
136    pub subject: String,
137
138    /// Token/credential issuer (e.g., `iss` claim in JWT)
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub issuer: Option<String>,
141
142    /// Intended audience (e.g., `aud` claim in JWT)
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub audience: Option<String>,
145
146    /// Expiration timestamp (Unix epoch seconds)
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub expires_at: Option<u64>,
149
150    /// Email address if available
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub email: Option<String>,
153
154    /// Display name if available
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub name: Option<String>,
157
158    /// Roles/permissions granted
159    #[serde(default, skip_serializing_if = "Vec::is_empty")]
160    pub roles: Vec<String>,
161
162    /// Additional claims (platform-specific)
163    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
164    pub claims: BTreeMap<String, serde_json::Value>,
165}
166
167impl Principal {
168    /// Create a new principal with just a subject
169    pub fn new(subject: impl Into<String>) -> Self {
170        Self {
171            subject: subject.into(),
172            issuer: None,
173            audience: None,
174            expires_at: None,
175            email: None,
176            name: None,
177            roles: Vec::new(),
178            claims: BTreeMap::new(),
179        }
180    }
181
182    /// Builder: set issuer
183    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
184        self.issuer = Some(issuer.into());
185        self
186    }
187
188    /// Builder: set audience
189    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
190        self.audience = Some(audience.into());
191        self
192    }
193
194    /// Builder: set expiration
195    pub fn with_expires_at(mut self, expires_at: u64) -> Self {
196        self.expires_at = Some(expires_at);
197        self
198    }
199
200    /// Builder: set email
201    pub fn with_email(mut self, email: impl Into<String>) -> Self {
202        self.email = Some(email.into());
203        self
204    }
205
206    /// Builder: set name
207    pub fn with_name(mut self, name: impl Into<String>) -> Self {
208        self.name = Some(name.into());
209        self
210    }
211
212    /// Builder: add a role
213    pub fn with_role(mut self, role: impl Into<String>) -> Self {
214        self.roles.push(role.into());
215        self
216    }
217
218    /// Builder: add multiple roles
219    pub fn with_roles(mut self, roles: impl IntoIterator<Item = impl Into<String>>) -> Self {
220        self.roles.extend(roles.into_iter().map(Into::into));
221        self
222    }
223
224    /// Builder: add a custom claim
225    pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
226        self.claims.insert(key.into(), value);
227        self
228    }
229
230    /// Check if principal has a specific role
231    pub fn has_role(&self, role: &str) -> bool {
232        self.roles.iter().any(|r| r == role)
233    }
234
235    /// Check if principal has any of the specified roles
236    pub fn has_any_role(&self, roles: &[&str]) -> bool {
237        roles.iter().any(|r| self.has_role(r))
238    }
239
240    /// Check if the principal is expired
241    ///
242    /// Note: In `no_std` environments, this always returns `false` since
243    /// there's no standard way to get the current time. Use
244    /// [`Principal::expires_at`] to check expiration manually.
245    pub fn is_expired(&self) -> bool {
246        #[cfg(feature = "std")]
247        {
248            if let Some(exp) = self.expires_at {
249                use std::time::{SystemTime, UNIX_EPOCH};
250                let now = SystemTime::now()
251                    .duration_since(UNIX_EPOCH)
252                    .map(|d| d.as_secs())
253                    .unwrap_or(0);
254                now > exp
255            } else {
256                false
257            }
258        }
259        #[cfg(not(feature = "std"))]
260        {
261            // In no_std, caller must check expiration externally
262            // since there's no standard way to get current time
263            let _ = self.expires_at;
264            false
265        }
266    }
267
268    /// Get a custom claim by key
269    pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
270        self.claims.get(key)
271    }
272}
273
274impl fmt::Display for Principal {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        write!(f, "Principal({})", self.subject)
277    }
278}
279
280/// Authentication error types.
281#[derive(Debug, Clone)]
282pub enum AuthError {
283    /// No credentials provided
284    MissingCredentials,
285
286    /// Invalid credential format
287    InvalidCredentialFormat(String),
288
289    /// Credential type not supported
290    UnsupportedCredentialType,
291
292    /// Token has expired
293    TokenExpired,
294
295    /// Token signature is invalid
296    InvalidSignature,
297
298    /// Token claims validation failed
299    InvalidClaims(String),
300
301    /// Issuer mismatch
302    InvalidIssuer {
303        /// Expected issuer
304        expected: String,
305        /// Actual issuer received
306        actual: String,
307    },
308
309    /// Audience mismatch
310    InvalidAudience {
311        /// Expected audience
312        expected: String,
313        /// Actual audience received
314        actual: String,
315    },
316
317    /// Key not found (e.g., kid not in JWKS)
318    KeyNotFound(String),
319
320    /// Failed to fetch JWKS or other key material
321    KeyFetchError(String),
322
323    /// Internal error
324    Internal(String),
325}
326
327impl fmt::Display for AuthError {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        match self {
330            Self::MissingCredentials => write!(f, "No credentials provided"),
331            Self::InvalidCredentialFormat(msg) => write!(f, "Invalid credential format: {}", msg),
332            Self::UnsupportedCredentialType => write!(f, "Unsupported credential type"),
333            Self::TokenExpired => write!(f, "Token has expired"),
334            Self::InvalidSignature => write!(f, "Invalid token signature"),
335            Self::InvalidClaims(msg) => write!(f, "Invalid claims: {}", msg),
336            Self::InvalidIssuer { expected, actual } => {
337                write!(
338                    f,
339                    "Invalid issuer: expected '{}', got '{}'",
340                    expected, actual
341                )
342            }
343            Self::InvalidAudience { expected, actual } => {
344                write!(
345                    f,
346                    "Invalid audience: expected '{}', got '{}'",
347                    expected, actual
348                )
349            }
350            Self::KeyNotFound(kid) => write!(f, "Key not found: {}", kid),
351            Self::KeyFetchError(msg) => write!(f, "Failed to fetch keys: {}", msg),
352            Self::Internal(msg) => write!(f, "Internal auth error: {}", msg),
353        }
354    }
355}
356
357#[cfg(feature = "std")]
358impl std::error::Error for AuthError {}
359
360/// Core authentication trait.
361///
362/// Implementors validate credentials and return an authenticated [`Principal`].
363/// This trait is platform-adaptive using [`MaybeSend`] bounds.
364///
365/// # Platform Implementations
366///
367/// - **Native** (`turbomcp-auth`): Uses `jsonwebtoken` crate with tokio
368/// - **WASM** (`turbomcp-wasm`): Uses Web Crypto API via `web-sys`
369///
370/// # Example
371///
372/// ```rust,ignore
373/// use turbomcp_core::auth::{Authenticator, AuthError, Credential, Principal};
374///
375/// struct ApiKeyAuth {
376///     valid_keys: Vec<String>,
377/// }
378///
379/// impl Authenticator for ApiKeyAuth {
380///     type Error = AuthError;
381///
382///     async fn authenticate(&self, credential: &Credential) -> Result<Principal, Self::Error> {
383///         let key = credential.as_bearer()
384///             .or_else(|| match credential {
385///                 Credential::ApiKey(k) => Some(k.as_str()),
386///                 _ => None,
387///             })
388///             .ok_or(AuthError::UnsupportedCredentialType)?;
389///
390///         if self.valid_keys.contains(&key.to_string()) {
391///             Ok(Principal::new("api-user"))
392///         } else {
393///             Err(AuthError::InvalidSignature)
394///         }
395///     }
396/// }
397/// ```
398pub trait Authenticator: MaybeSend + MaybeSync + Clone {
399    /// Error type returned on authentication failure
400    type Error: fmt::Debug + fmt::Display + MaybeSend;
401
402    /// Validate a credential and return the authenticated principal.
403    ///
404    /// # Arguments
405    ///
406    /// * `credential` - The credential to validate
407    ///
408    /// # Returns
409    ///
410    /// * `Ok(Principal)` - Authentication successful
411    /// * `Err(Self::Error)` - Authentication failed
412    fn authenticate(
413        &self,
414        credential: &Credential,
415    ) -> impl Future<Output = Result<Principal, Self::Error>> + MaybeSend;
416}
417
418/// Extracts credentials from request context.
419///
420/// Implementors extract authentication credentials from various sources
421/// (headers, query params, cookies, etc.).
422///
423/// # Default Implementation
424///
425/// [`HeaderExtractor`] extracts from the `Authorization` header:
426/// - `Bearer <token>` → [`Credential::Bearer`]
427/// - `Basic <base64>` → [`Credential::Basic`]
428/// - `ApiKey <key>` → [`Credential::ApiKey`]
429pub trait CredentialExtractor: MaybeSend + MaybeSync {
430    /// Extract credentials from the given headers.
431    ///
432    /// # Arguments
433    ///
434    /// * `get_header` - Function to retrieve header value by name
435    ///
436    /// # Returns
437    ///
438    /// * `Some(Credential)` - Credential found
439    /// * `None` - No credential present
440    fn extract<F>(&self, get_header: F) -> Option<Credential>
441    where
442        F: Fn(&str) -> Option<String>;
443}
444
445/// Default credential extractor that reads the Authorization header.
446#[derive(Debug, Clone, Copy, Default)]
447pub struct HeaderExtractor;
448
449impl CredentialExtractor for HeaderExtractor {
450    fn extract<F>(&self, get_header: F) -> Option<Credential>
451    where
452        F: Fn(&str) -> Option<String>,
453    {
454        // Try Authorization header first
455        if let Some(auth) = get_header("authorization") {
456            let auth = auth.trim();
457
458            // Bearer token
459            if let Some(token) = auth
460                .strip_prefix("Bearer ")
461                .or_else(|| auth.strip_prefix("bearer "))
462            {
463                return Some(Credential::Bearer(token.trim().to_string()));
464            }
465
466            // Basic auth - requires std feature for base64 decoding
467            #[cfg(feature = "std")]
468            if let Some(encoded) = auth
469                .strip_prefix("Basic ")
470                .or_else(|| auth.strip_prefix("basic "))
471            {
472                use base64::Engine;
473                if let Ok(decoded) =
474                    base64::engine::general_purpose::STANDARD.decode(encoded.trim())
475                    && let Ok(decoded_str) = String::from_utf8(decoded)
476                    && let Some((username, password)) = decoded_str.split_once(':')
477                {
478                    return Some(Credential::Basic {
479                        username: username.to_string(),
480                        password: password.to_string(),
481                    });
482                }
483            }
484
485            // ApiKey scheme
486            if let Some(key) = auth
487                .strip_prefix("ApiKey ")
488                .or_else(|| auth.strip_prefix("apikey "))
489            {
490                return Some(Credential::ApiKey(key.trim().to_string()));
491            }
492        }
493
494        // Try X-API-Key header
495        if let Some(key) = get_header("x-api-key") {
496            return Some(Credential::ApiKey(key.trim().to_string()));
497        }
498
499        None
500    }
501}
502
503/// JWT validation configuration.
504///
505/// Platform-agnostic configuration for JWT validation.
506///
507/// # Security
508///
509/// **IMPORTANT**: Always use [`JwtConfig::new()`] to create a configuration with
510/// secure defaults. The `Default` trait is intentionally NOT implemented to prevent
511/// accidental creation of insecure configurations with empty algorithm whitelists.
512///
513/// ```rust
514/// use turbomcp_core::auth::{JwtConfig, JwtAlgorithm};
515///
516/// // ✅ CORRECT: Use new() for secure defaults (RS256, ES256)
517/// let config = JwtConfig::new()
518///     .issuer("https://auth.example.com")
519///     .audience("my-api");
520///
521/// // ✅ CORRECT: Explicitly specify algorithms
522/// let config = JwtConfig::new()
523///     .algorithms(vec![JwtAlgorithm::RS256]);
524/// ```
525#[derive(Debug, Clone)]
526pub struct JwtConfig {
527    /// Expected issuer (`iss` claim)
528    pub issuer: Option<String>,
529
530    /// Expected audience (`aud` claim)
531    pub audience: Option<String>,
532
533    /// Allowed signing algorithms.
534    ///
535    /// **Security**: This list MUST NOT be empty. An empty list will cause
536    /// token validation to fail with an error. Always specify at least one
537    /// algorithm explicitly.
538    pub algorithms: Vec<JwtAlgorithm>,
539
540    /// Clock skew tolerance in seconds (default: 60)
541    pub leeway_seconds: u64,
542
543    /// Whether to validate expiration (default: true)
544    pub validate_exp: bool,
545
546    /// Whether to validate not-before (default: true)
547    pub validate_nbf: bool,
548}
549
550impl JwtConfig {
551    /// Create a new JWT config with sensible defaults.
552    ///
553    /// # Security
554    ///
555    /// This method provides secure defaults:
556    /// - `algorithms`: `[RS256, ES256]` (asymmetric algorithms only)
557    /// - `validate_exp`: `true`
558    /// - `validate_nbf`: `true`
559    /// - `leeway_seconds`: `60`
560    ///
561    /// The `Default` trait is intentionally NOT implemented to prevent
562    /// accidental creation of configurations with an empty algorithm list,
563    /// which would bypass algorithm validation and enable algorithm
564    /// confusion attacks.
565    #[allow(clippy::new_without_default)]
566    pub fn new() -> Self {
567        Self {
568            issuer: None,
569            audience: None,
570            algorithms: vec![JwtAlgorithm::RS256, JwtAlgorithm::ES256],
571            leeway_seconds: 60,
572            validate_exp: true,
573            validate_nbf: true,
574        }
575    }
576
577    /// Builder: set expected issuer
578    pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
579        self.issuer = Some(issuer.into());
580        self
581    }
582
583    /// Builder: set expected audience
584    pub fn audience(mut self, audience: impl Into<String>) -> Self {
585        self.audience = Some(audience.into());
586        self
587    }
588
589    /// Builder: set allowed algorithms
590    pub fn algorithms(mut self, algorithms: Vec<JwtAlgorithm>) -> Self {
591        self.algorithms = algorithms;
592        self
593    }
594
595    /// Builder: set clock skew leeway
596    pub fn leeway_seconds(mut self, seconds: u64) -> Self {
597        self.leeway_seconds = seconds;
598        self
599    }
600
601    /// Builder: disable expiration validation
602    pub fn skip_exp_validation(mut self) -> Self {
603        self.validate_exp = false;
604        self
605    }
606
607    /// Builder: disable not-before validation
608    pub fn skip_nbf_validation(mut self) -> Self {
609        self.validate_nbf = false;
610        self
611    }
612}
613
614/// JWT signing algorithms supported across platforms.
615///
616/// This is a subset of algorithms that work on both native and WASM.
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
618#[serde(rename_all = "UPPERCASE")]
619pub enum JwtAlgorithm {
620    /// HMAC with SHA-256
621    HS256,
622    /// HMAC with SHA-384
623    HS384,
624    /// HMAC with SHA-512
625    HS512,
626    /// RSA PKCS#1 with SHA-256
627    RS256,
628    /// RSA PKCS#1 with SHA-384
629    RS384,
630    /// RSA PKCS#1 with SHA-512
631    RS512,
632    /// ECDSA with P-256 and SHA-256
633    ES256,
634    /// ECDSA with P-384 and SHA-384
635    ES384,
636}
637
638impl JwtAlgorithm {
639    /// Returns the algorithm name as used in JWT headers
640    pub fn as_str(&self) -> &'static str {
641        match self {
642            Self::HS256 => "HS256",
643            Self::HS384 => "HS384",
644            Self::HS512 => "HS512",
645            Self::RS256 => "RS256",
646            Self::RS384 => "RS384",
647            Self::RS512 => "RS512",
648            Self::ES256 => "ES256",
649            Self::ES384 => "ES384",
650        }
651    }
652
653    /// Parse algorithm from string (convenience method)
654    ///
655    /// Returns `None` if the string is not a recognized algorithm.
656    /// For infallible parsing, use `str::parse::<JwtAlgorithm>()`.
657    pub fn parse(s: &str) -> Option<Self> {
658        s.parse().ok()
659    }
660
661    /// Check if this is an asymmetric algorithm (RSA or ECDSA)
662    pub fn is_asymmetric(&self) -> bool {
663        matches!(
664            self,
665            Self::RS256 | Self::RS384 | Self::RS512 | Self::ES256 | Self::ES384
666        )
667    }
668
669    /// Check if this is a symmetric algorithm (HMAC)
670    pub fn is_symmetric(&self) -> bool {
671        matches!(self, Self::HS256 | Self::HS384 | Self::HS512)
672    }
673}
674
675impl fmt::Display for JwtAlgorithm {
676    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
677        write!(f, "{}", self.as_str())
678    }
679}
680
681impl core::str::FromStr for JwtAlgorithm {
682    type Err = ();
683
684    fn from_str(s: &str) -> Result<Self, Self::Err> {
685        match s.to_uppercase().as_str() {
686            "HS256" => Ok(Self::HS256),
687            "HS384" => Ok(Self::HS384),
688            "HS512" => Ok(Self::HS512),
689            "RS256" => Ok(Self::RS256),
690            "RS384" => Ok(Self::RS384),
691            "RS512" => Ok(Self::RS512),
692            "ES256" => Ok(Self::ES256),
693            "ES384" => Ok(Self::ES384),
694            _ => Err(()),
695        }
696    }
697}
698
699/// Standard JWT claims.
700///
701/// These are the registered claims from RFC 7519.
702#[derive(Debug, Clone, Default, Serialize, Deserialize)]
703pub struct StandardClaims {
704    /// Subject (user identifier)
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub sub: Option<String>,
707
708    /// Issuer
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub iss: Option<String>,
711
712    /// Audience (can be string or array)
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub aud: Option<Audience>,
715
716    /// Expiration time (Unix timestamp)
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub exp: Option<u64>,
719
720    /// Not before time (Unix timestamp)
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub nbf: Option<u64>,
723
724    /// Issued at time (Unix timestamp)
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub iat: Option<u64>,
727
728    /// JWT ID
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub jti: Option<String>,
731}
732
733/// JWT audience claim (can be a single string or array of strings).
734#[derive(Debug, Clone, Serialize, Deserialize)]
735#[serde(untagged)]
736pub enum Audience {
737    /// Single audience
738    Single(String),
739    /// Multiple audiences
740    Multiple(Vec<String>),
741}
742
743impl Audience {
744    /// Check if the audience contains a specific value
745    pub fn contains(&self, expected: &str) -> bool {
746        match self {
747            Self::Single(s) => s == expected,
748            Self::Multiple(v) => v.iter().any(|s| s == expected),
749        }
750    }
751
752    /// Convert to a vector of strings
753    pub fn to_vec(&self) -> Vec<String> {
754        match self {
755            Self::Single(s) => vec![s.clone()],
756            Self::Multiple(v) => v.clone(),
757        }
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn test_credential_constructors() {
767        let bearer = Credential::bearer("token123");
768        assert!(bearer.is_bearer());
769        assert_eq!(bearer.as_bearer(), Some("token123"));
770
771        let api_key = Credential::api_key("key456");
772        assert!(!api_key.is_bearer());
773        assert_eq!(api_key.as_bearer(), None);
774
775        let basic = Credential::basic("user", "pass");
776        assert!(!basic.is_bearer());
777    }
778
779    #[test]
780    fn test_principal_builder() {
781        let principal = Principal::new("user123")
782            .with_issuer("https://auth.example.com")
783            .with_audience("my-api")
784            .with_email("user@example.com")
785            .with_role("admin")
786            .with_role("user");
787
788        assert_eq!(principal.subject, "user123");
789        assert_eq!(
790            principal.issuer,
791            Some("https://auth.example.com".to_string())
792        );
793        assert!(principal.has_role("admin"));
794        assert!(principal.has_role("user"));
795        assert!(!principal.has_role("guest"));
796        assert!(principal.has_any_role(&["admin", "guest"]));
797    }
798
799    #[test]
800    fn test_header_extractor_bearer() {
801        let extractor = HeaderExtractor;
802
803        let cred = extractor.extract(|name| {
804            if name == "authorization" {
805                Some("Bearer my-token".to_string())
806            } else {
807                None
808            }
809        });
810
811        assert_eq!(cred, Some(Credential::Bearer("my-token".to_string())));
812    }
813
814    #[test]
815    fn test_header_extractor_api_key() {
816        let extractor = HeaderExtractor;
817
818        // Via X-API-Key header
819        let cred = extractor.extract(|name| {
820            if name == "x-api-key" {
821                Some("my-api-key".to_string())
822            } else {
823                None
824            }
825        });
826
827        assert_eq!(cred, Some(Credential::ApiKey("my-api-key".to_string())));
828
829        // Via Authorization: ApiKey
830        let cred2 = extractor.extract(|name| {
831            if name == "authorization" {
832                Some("ApiKey another-key".to_string())
833            } else {
834                None
835            }
836        });
837
838        assert_eq!(cred2, Some(Credential::ApiKey("another-key".to_string())));
839    }
840
841    #[test]
842    fn test_jwt_algorithm() {
843        assert_eq!(JwtAlgorithm::RS256.as_str(), "RS256");
844        assert!(JwtAlgorithm::RS256.is_asymmetric());
845        assert!(!JwtAlgorithm::RS256.is_symmetric());
846
847        assert!(JwtAlgorithm::HS256.is_symmetric());
848        assert!(!JwtAlgorithm::HS256.is_asymmetric());
849
850        assert_eq!(JwtAlgorithm::parse("es256"), Some(JwtAlgorithm::ES256));
851        assert_eq!(JwtAlgorithm::parse("unknown"), None);
852    }
853
854    #[test]
855    fn test_audience() {
856        let single = Audience::Single("my-api".to_string());
857        assert!(single.contains("my-api"));
858        assert!(!single.contains("other"));
859
860        let multiple = Audience::Multiple(vec!["api1".to_string(), "api2".to_string()]);
861        assert!(multiple.contains("api1"));
862        assert!(multiple.contains("api2"));
863        assert!(!multiple.contains("api3"));
864    }
865
866    #[test]
867    fn test_jwt_config_builder() {
868        let config = JwtConfig::new()
869            .issuer("https://auth.example.com")
870            .audience("my-api")
871            .algorithms(vec![JwtAlgorithm::RS256])
872            .leeway_seconds(120);
873
874        assert_eq!(config.issuer, Some("https://auth.example.com".to_string()));
875        assert_eq!(config.audience, Some("my-api".to_string()));
876        assert_eq!(config.algorithms, vec![JwtAlgorithm::RS256]);
877        assert_eq!(config.leeway_seconds, 120);
878    }
879}