hessra_api/
lib.rs

1//! # Hessra API
2//!
3//! HTTP client for Hessra authentication services.
4//!
5//! This crate provides a client for making HTTP requests to the Hessra
6//! authorization service. It supports both HTTP/1.1 and HTTP/3 (as an optional feature)
7//! and implements the OpenAPI specification for the Hessra service.
8//!
9//! ## Features
10//!
11//! - HTTP/1.1 client for Hessra services
12//! - Optional HTTP/3 support
13//! - Implementation of all Hessra API endpoints
14//! - Mutual TLS (mTLS) for secure client authentication
15//! - Identity token support for authentication without mTLS (except initial issuance)
16//! - Bearer token authentication using identity tokens
17
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21use hessra_config::{HessraConfig, Protocol};
22
23// Error type for the API client
24#[derive(Error, Debug)]
25pub enum ApiError {
26    #[error("HTTP client error: {0}")]
27    HttpClient(#[from] reqwest::Error),
28
29    #[error("SSL configuration error: {0}")]
30    SslConfig(String),
31
32    #[error("Invalid response: {0}")]
33    InvalidResponse(String),
34
35    #[error("Token request error: {0}")]
36    TokenRequest(String),
37
38    #[error("Token verification error: {0}")]
39    TokenVerification(String),
40
41    #[error("Service chain error: {0}")]
42    ServiceChain(String),
43
44    #[error("Internal error: {0}")]
45    Internal(String),
46
47    #[error("Signoff failed: {0}")]
48    SignoffFailed(String),
49
50    #[error("Missing signoff configuration for service: {0}")]
51    MissingSignoffConfig(String),
52
53    #[error("Invalid signoff response from {service}: {reason}")]
54    InvalidSignoffResponse { service: String, reason: String },
55
56    #[error("Signoff collection incomplete: {missing_signoffs} signoffs remaining")]
57    IncompleteSignoffs { missing_signoffs: usize },
58}
59
60// Request and response structures
61/// Request payload for requesting an authorization token
62#[derive(Serialize, Deserialize)]
63pub struct TokenRequest {
64    /// The resource identifier to request authorization for
65    pub resource: String,
66    /// The operation to request authorization for
67    pub operation: String,
68}
69
70/// Request payload for verifying an authorization token
71#[derive(Serialize, Deserialize)]
72pub struct VerifyTokenRequest {
73    /// The authorization token to verify
74    pub token: String,
75    /// The subject identifier to verify against
76    pub subject: String,
77    /// The resource identifier to verify authorization against
78    pub resource: String,
79    /// The operation to verify authorization for
80    pub operation: String,
81}
82
83/// Information about required signoffs for multi-party tokens
84#[derive(Serialize, Deserialize, Debug, Clone)]
85pub struct SignoffInfo {
86    pub component: String,
87    pub authorization_service: String,
88    pub public_key: String,
89}
90
91/// Request structure for token signing operations
92#[derive(Serialize, Deserialize, Debug, Clone)]
93pub struct SignTokenRequest {
94    pub token: String,
95    pub resource: String,
96    pub operation: String,
97}
98
99/// Response structure for token signing operations
100#[derive(Serialize, Deserialize, Debug, Clone)]
101pub struct SignTokenResponse {
102    pub response_msg: String,
103    pub signed_token: Option<String>,
104}
105
106/// Enhanced token response that may include pending signoffs
107#[derive(Serialize, Deserialize, Debug, Clone)]
108pub struct TokenResponse {
109    /// Response message from the server
110    pub response_msg: String,
111    /// The issued token, if successful
112    pub token: Option<String>,
113    /// Pending signoffs required for multi-party tokens
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub pending_signoffs: Option<Vec<SignoffInfo>>,
116}
117
118/// Response from a token verification operation
119#[derive(Serialize, Deserialize)]
120pub struct VerifyTokenResponse {
121    /// Response message from the server
122    pub response_msg: String,
123}
124
125/// Response from a public key request
126#[derive(Serialize, Deserialize)]
127pub struct PublicKeyResponse {
128    pub response_msg: String,
129    pub public_key: String,
130}
131
132/// Response from a CA certificate request
133#[derive(Serialize, Deserialize)]
134pub struct CaCertResponse {
135    pub response_msg: String,
136    pub ca_cert_pem: String,
137}
138
139/// Request payload for verifying a service chain token
140#[derive(Serialize, Deserialize)]
141pub struct VerifyServiceChainTokenRequest {
142    pub token: String,
143    pub subject: String,
144    pub resource: String,
145    pub component: Option<String>,
146}
147
148/// Request for minting a new identity token
149#[derive(Serialize, Deserialize)]
150pub struct IdentityTokenRequest {
151    /// Optional identifier - required for token-only auth, optional for mTLS
152    pub identifier: Option<String>,
153}
154
155/// Request for refreshing an existing identity token
156#[derive(Serialize, Deserialize)]
157pub struct RefreshIdentityTokenRequest {
158    /// The current identity token to refresh
159    pub current_token: String,
160    /// Optional identifier - required for token-only auth, optional for mTLS
161    pub identifier: Option<String>,
162}
163
164/// Response from identity token operations
165#[derive(Serialize, Deserialize, Debug, Clone)]
166pub struct IdentityTokenResponse {
167    /// Response message from the server
168    pub response_msg: String,
169    /// The issued identity token, if successful
170    pub token: Option<String>,
171    /// Time until expiration in seconds
172    pub expires_in: Option<u64>,
173    /// The identity contained in the token
174    pub identity: Option<String>,
175}
176
177/// Base configuration for Hessra clients
178#[derive(Clone)]
179pub struct BaseConfig {
180    /// Base URL of the Hessra service (without protocol scheme)
181    pub base_url: String,
182    /// Optional port to connect to
183    pub port: Option<u16>,
184    /// Optional mTLS private key in PEM format (required for mTLS auth)
185    pub mtls_key: Option<String>,
186    /// Optional mTLS client certificate in PEM format (required for mTLS auth)
187    pub mtls_cert: Option<String>,
188    /// Server CA certificate in PEM format
189    pub server_ca: String,
190    /// Public key for token verification in PEM format
191    pub public_key: Option<String>,
192    /// Personal keypair for service chain attestation
193    pub personal_keypair: Option<String>,
194}
195
196impl BaseConfig {
197    /// Get the formatted base URL, with port if specified
198    pub fn get_base_url(&self) -> String {
199        match self.port {
200            Some(port) => format!("{}:{port}", self.base_url),
201            None => self.base_url.clone(),
202        }
203    }
204}
205
206/// HTTP/1.1 client implementation
207pub struct Http1Client {
208    /// Base configuration
209    config: BaseConfig,
210    /// reqwest HTTP client with mTLS configured
211    client: reqwest::Client,
212}
213
214impl Http1Client {
215    /// Create a new HTTP/1.1 client with the given configuration
216    pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
217        // Parse the CA certificate chain (may contain root + intermediates + leaf)
218        let certs =
219            reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
220                ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
221            })?;
222
223        // Build the client with or without mTLS depending on configuration
224        let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
225
226        // Add all certificates from the chain as trusted roots
227        for cert in certs {
228            client_builder = client_builder.add_root_certificate(cert);
229        }
230
231        // Add mTLS identity if both cert and key are provided
232        if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
233            let identity_str = format!("{cert}{key}");
234            let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
235                ApiError::SslConfig(format!(
236                    "Failed to create identity from certificate and key: {e}"
237                ))
238            })?;
239            client_builder = client_builder.identity(identity);
240        }
241
242        let client = client_builder
243            .build()
244            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
245
246        Ok(Self { config, client })
247    }
248
249    /// Send a request to the remote Hessra authorization service
250    pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
251    where
252        T: Serialize,
253        R: for<'de> Deserialize<'de>,
254    {
255        let base_url = self.config.get_base_url();
256        let url = format!("https://{base_url}/{endpoint}");
257
258        let response = self
259            .client
260            .post(&url)
261            .json(request_body)
262            .send()
263            .await
264            .map_err(ApiError::HttpClient)?;
265
266        if !response.status().is_success() {
267            let status = response.status();
268            let error_text = response.text().await.unwrap_or_default();
269            return Err(ApiError::InvalidResponse(format!(
270                "HTTP error: {status} - {error_text}"
271            )));
272        }
273
274        let result = response
275            .json::<R>()
276            .await
277            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
278
279        Ok(result)
280    }
281
282    pub async fn send_request_with_auth<T, R>(
283        &self,
284        endpoint: &str,
285        request_body: &T,
286        auth_header: &str,
287    ) -> Result<R, ApiError>
288    where
289        T: Serialize,
290        R: for<'de> Deserialize<'de>,
291    {
292        let base_url = self.config.get_base_url();
293        let url = format!("https://{base_url}/{endpoint}");
294
295        let response = self
296            .client
297            .post(&url)
298            .header("Authorization", auth_header)
299            .json(request_body)
300            .send()
301            .await
302            .map_err(ApiError::HttpClient)?;
303
304        if !response.status().is_success() {
305            let status = response.status();
306            let error_text = response.text().await.unwrap_or_default();
307            return Err(ApiError::InvalidResponse(format!(
308                "HTTP error: {status} - {error_text}"
309            )));
310        }
311
312        let result = response
313            .json::<R>()
314            .await
315            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
316
317        Ok(result)
318    }
319}
320
321/// HTTP/3 client implementation (only available with the "http3" feature)
322#[cfg(feature = "http3")]
323pub struct Http3Client {
324    /// Base configuration
325    config: BaseConfig,
326    /// QUIC endpoint for HTTP/3 connections
327    client: reqwest::Client,
328}
329
330#[cfg(feature = "http3")]
331impl Http3Client {
332    /// Create a new HTTP/3 client with the given configuration
333    pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
334        // Parse the CA certificate chain (may contain root + intermediates + leaf)
335        let certs =
336            reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
337                ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
338            })?;
339
340        // Build the client with or without mTLS depending on configuration
341        let mut client_builder = reqwest::ClientBuilder::new()
342            .use_rustls_tls()
343            .http3_prior_knowledge();
344
345        // Add all certificates from the chain as trusted roots
346        for cert in certs {
347            client_builder = client_builder.add_root_certificate(cert);
348        }
349
350        // Add mTLS identity if both cert and key are provided
351        if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
352            let identity_str = format!("{}{}", cert, key);
353            let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
354                ApiError::SslConfig(format!(
355                    "Failed to create identity from certificate and key: {e}"
356                ))
357            })?;
358            client_builder = client_builder.identity(identity);
359        }
360
361        let client = client_builder
362            .build()
363            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
364
365        Ok(Self { config, client })
366    }
367
368    /// Send a request to the Hessra service
369    pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
370    where
371        T: Serialize,
372        R: for<'de> Deserialize<'de>,
373    {
374        let base_url = self.config.get_base_url();
375        let url = format!("https://{base_url}/{endpoint}");
376
377        let response = self
378            .client
379            .post(&url)
380            .json(request_body)
381            .send()
382            .await
383            .map_err(ApiError::HttpClient)?;
384
385        if !response.status().is_success() {
386            let status = response.status();
387            let error_text = response.text().await.unwrap_or_default();
388            return Err(ApiError::InvalidResponse(format!(
389                "HTTP error: {status} - {error_text}"
390            )));
391        }
392
393        let result = response
394            .json::<R>()
395            .await
396            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
397
398        Ok(result)
399    }
400
401    pub async fn send_request_with_auth<T, R>(
402        &self,
403        endpoint: &str,
404        request_body: &T,
405        auth_header: &str,
406    ) -> Result<R, ApiError>
407    where
408        T: Serialize,
409        R: for<'de> Deserialize<'de>,
410    {
411        let base_url = self.config.get_base_url();
412        let url = format!("https://{base_url}/{endpoint}");
413
414        let response = self
415            .client
416            .post(&url)
417            .header("Authorization", auth_header)
418            .json(request_body)
419            .send()
420            .await
421            .map_err(ApiError::HttpClient)?;
422
423        if !response.status().is_success() {
424            let status = response.status();
425            let error_text = response.text().await.unwrap_or_default();
426            return Err(ApiError::InvalidResponse(format!(
427                "HTTP error: {status} - {error_text}"
428            )));
429        }
430
431        let result = response
432            .json::<R>()
433            .await
434            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
435
436        Ok(result)
437    }
438}
439
440/// The main Hessra client type providing token request and verification
441pub enum HessraClient {
442    /// HTTP/1.1 client
443    Http1(Http1Client),
444    /// HTTP/3 client (only available with the "http3" feature)
445    #[cfg(feature = "http3")]
446    Http3(Http3Client),
447}
448
449/// Builder for creating Hessra clients
450pub struct HessraClientBuilder {
451    /// Base configuration being built
452    config: BaseConfig,
453    /// Protocol to use for the client
454    protocol: hessra_config::Protocol,
455}
456
457impl HessraClientBuilder {
458    /// Create a new client builder with default values
459    pub fn new() -> Self {
460        Self {
461            config: BaseConfig {
462                base_url: String::new(),
463                port: None,
464                mtls_key: None,
465                mtls_cert: None,
466                server_ca: String::new(),
467                public_key: None,
468                personal_keypair: None,
469            },
470            protocol: Protocol::Http1,
471        }
472    }
473
474    /// Create a client builder from a HessraConfig
475    pub fn from_config(mut self, config: &HessraConfig) -> Self {
476        self.config.base_url = config.base_url.clone();
477        self.config.port = config.port;
478        self.config.mtls_key = config.mtls_key.clone();
479        self.config.mtls_cert = config.mtls_cert.clone();
480        self.config.server_ca = config.server_ca.clone();
481        self.config.public_key = config.public_key.clone();
482        self.config.personal_keypair = config.personal_keypair.clone();
483        self.protocol = config.protocol.clone();
484        self
485    }
486
487    /// Set the base URL for the client, e.g. "test.hessra.net"
488    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
489        self.config.base_url = base_url.into();
490        self
491    }
492
493    /// Set the mTLS private key for the client
494    /// PEM formatted string
495    pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
496        self.config.mtls_key = Some(mtls_key.into());
497        self
498    }
499
500    /// Set the mTLS certificate for the client
501    /// PEM formatted string
502    pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
503        self.config.mtls_cert = Some(mtls_cert.into());
504        self
505    }
506
507    /// Set the server CA certificate for the client
508    /// PEM formatted string
509    pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
510        self.config.server_ca = server_ca.into();
511        self
512    }
513
514    /// Set the port for the client
515    pub fn port(mut self, port: u16) -> Self {
516        self.config.port = Some(port);
517        self
518    }
519
520    /// Set the protocol for the client
521    pub fn protocol(mut self, protocol: Protocol) -> Self {
522        self.protocol = protocol;
523        self
524    }
525
526    /// Set the public key for token verification
527    /// PEM formatted string. note, this is JUST the public key, not the entire keypair.
528    pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
529        self.config.public_key = Some(public_key.into());
530        self
531    }
532
533    /// Set the personal keypair for service chain attestation
534    /// PEM formatted string. note, this is the entire keypair
535    /// and needs to be kept secret.
536    pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
537        self.config.personal_keypair = Some(keypair.into());
538        self
539    }
540
541    /// Build the HTTP/1.1 client
542    fn build_http1(&self) -> Result<Http1Client, ApiError> {
543        Http1Client::new(self.config.clone())
544    }
545
546    /// Build the HTTP/3 client
547    #[cfg(feature = "http3")]
548    fn build_http3(&self) -> Result<Http3Client, ApiError> {
549        Http3Client::new(self.config.clone())
550    }
551
552    /// Build the client
553    pub fn build(self) -> Result<HessraClient, ApiError> {
554        match self.protocol {
555            Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
556            #[cfg(feature = "http3")]
557            Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
558            #[allow(unreachable_patterns)]
559            _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
560        }
561    }
562}
563
564impl Default for HessraClientBuilder {
565    fn default() -> Self {
566        Self::new()
567    }
568}
569
570impl HessraClient {
571    /// Create a new client builder
572    pub fn builder() -> HessraClientBuilder {
573        HessraClientBuilder::new()
574    }
575
576    /// Fetch the public key from the Hessra service without creating a client
577    /// The public_key endpoint is available as both an authenticated and unauthenticated
578    /// request.
579    pub async fn fetch_public_key(
580        base_url: impl Into<String>,
581        port: Option<u16>,
582        server_ca: impl Into<String>,
583    ) -> Result<String, ApiError> {
584        let base_url = base_url.into();
585        let server_ca = server_ca.into();
586
587        // Create a regular reqwest client (no mTLS)
588        let cert_pem = server_ca.as_bytes();
589        let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
590            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
591
592        let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
593        for cert in certs {
594            client_builder = client_builder.add_root_certificate(cert);
595        }
596
597        let client = client_builder
598            .build()
599            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
600
601        // Format the URL
602        let url = match port {
603            Some(port) => format!("https://{base_url}:{port}/public_key"),
604            None => format!("https://{base_url}/public_key"),
605        };
606
607        // Make the request
608        let response = client
609            .get(&url)
610            .send()
611            .await
612            .map_err(ApiError::HttpClient)?;
613
614        if !response.status().is_success() {
615            let status = response.status();
616            let error_text = response.text().await.unwrap_or_default();
617            return Err(ApiError::InvalidResponse(format!(
618                "HTTP error: {status} - {error_text}"
619            )));
620        }
621
622        // Parse the response
623        let result = response
624            .json::<PublicKeyResponse>()
625            .await
626            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
627
628        Ok(result.public_key)
629    }
630
631    /// Fetch the CA certificate from the Hessra service without authentication
632    ///
633    /// This function makes an unauthenticated request to the `/ca_cert` endpoint
634    /// to retrieve the server's CA certificate in PEM format. This is useful for
635    /// bootstrapping trust when setting up a new client.
636    ///
637    /// # Bootstrap Trust Considerations
638    ///
639    /// This function uses the system CA store for the initial connection. If the
640    /// server uses a self-signed certificate, consider using `fetch_ca_cert_insecure`
641    /// instead (with appropriate warnings to users).
642    pub async fn fetch_ca_cert(
643        base_url: impl Into<String>,
644        port: Option<u16>,
645    ) -> Result<String, ApiError> {
646        let base_url = base_url.into();
647
648        // Create a reqwest client using system CA store
649        let client = reqwest::ClientBuilder::new()
650            .use_rustls_tls()
651            .build()
652            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
653
654        // Format the URL
655        let url = match port {
656            Some(port) => format!("https://{base_url}:{port}/ca_cert"),
657            None => format!("https://{base_url}/ca_cert"),
658        };
659
660        // Make the request
661        let response = client
662            .get(&url)
663            .send()
664            .await
665            .map_err(ApiError::HttpClient)?;
666
667        if !response.status().is_success() {
668            let status = response.status();
669            let error_text = response.text().await.unwrap_or_default();
670            return Err(ApiError::InvalidResponse(format!(
671                "HTTP error: {status} - {error_text}"
672            )));
673        }
674
675        // Parse the response
676        let result = response
677            .json::<CaCertResponse>()
678            .await
679            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
680
681        // Validate it's a non-empty PEM certificate
682        if result.ca_cert_pem.is_empty() {
683            return Err(ApiError::InvalidResponse(
684                "Server returned empty CA certificate".to_string(),
685            ));
686        }
687
688        if !result.ca_cert_pem.contains("-----BEGIN CERTIFICATE-----") {
689            return Err(ApiError::InvalidResponse(
690                "Server returned invalid PEM format".to_string(),
691            ));
692        }
693
694        Ok(result.ca_cert_pem)
695    }
696
697    #[cfg(feature = "http3")]
698    pub async fn fetch_public_key_http3(
699        base_url: impl Into<String>,
700        port: Option<u16>,
701        server_ca: impl Into<String>,
702    ) -> Result<String, ApiError> {
703        let base_url = base_url.into();
704        let server_ca = server_ca.into();
705
706        // Create a regular reqwest client (no mTLS)
707        let cert_pem = server_ca.as_bytes();
708        let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
709            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
710
711        let mut client_builder = reqwest::ClientBuilder::new()
712            .use_rustls_tls()
713            .http3_prior_knowledge();
714        for cert in certs {
715            client_builder = client_builder.add_root_certificate(cert);
716        }
717
718        let client = client_builder
719            .build()
720            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
721
722        // Format the URL
723        let url = match port {
724            Some(port) => format!("https://{base_url}:{port}/public_key"),
725            None => format!("https://{base_url}/public_key"),
726        };
727
728        // Make the request
729        let response = client
730            .get(&url)
731            .send()
732            .await
733            .map_err(ApiError::HttpClient)?;
734
735        if !response.status().is_success() {
736            let status = response.status();
737            let error_text = response.text().await.unwrap_or_default();
738            return Err(ApiError::InvalidResponse(format!(
739                "HTTP error: {status} - {error_text}"
740            )));
741        }
742
743        // Parse the response
744        let result = response
745            .json::<PublicKeyResponse>()
746            .await
747            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
748
749        Ok(result.public_key)
750    }
751
752    /// Request a token for a resource
753    /// Returns the full TokenResponse which may include pending signoffs for multi-party tokens
754    pub async fn request_token(
755        &self,
756        resource: String,
757        operation: String,
758    ) -> Result<TokenResponse, ApiError> {
759        let request = TokenRequest {
760            resource,
761            operation,
762        };
763
764        let response = match self {
765            HessraClient::Http1(client) => {
766                client
767                    .send_request::<_, TokenResponse>("request_token", &request)
768                    .await?
769            }
770            #[cfg(feature = "http3")]
771            HessraClient::Http3(client) => {
772                client
773                    .send_request::<_, TokenResponse>("request_token", &request)
774                    .await?
775            }
776        };
777
778        Ok(response)
779    }
780
781    /// Request a token for a resource using an identity token for authentication
782    /// The identity token will be sent in the Authorization header as a Bearer token
783    /// Returns the full TokenResponse which may include pending signoffs for multi-party tokens
784    pub async fn request_token_with_identity(
785        &self,
786        resource: String,
787        operation: String,
788        identity_token: String,
789    ) -> Result<TokenResponse, ApiError> {
790        let request = TokenRequest {
791            resource,
792            operation,
793        };
794
795        let response = match self {
796            HessraClient::Http1(client) => {
797                client
798                    .send_request_with_auth::<_, TokenResponse>(
799                        "request_token",
800                        &request,
801                        &format!("Bearer {identity_token}"),
802                    )
803                    .await?
804            }
805            #[cfg(feature = "http3")]
806            HessraClient::Http3(client) => {
807                client
808                    .send_request_with_auth::<_, TokenResponse>(
809                        "request_token",
810                        &request,
811                        &format!("Bearer {identity_token}"),
812                    )
813                    .await?
814            }
815        };
816
817        Ok(response)
818    }
819
820    /// Request a token for a resource (legacy method)
821    /// This method returns just the token string for backward compatibility
822    pub async fn request_token_simple(
823        &self,
824        resource: String,
825        operation: String,
826    ) -> Result<String, ApiError> {
827        let response = self.request_token(resource, operation).await?;
828
829        match response.token {
830            Some(token) => Ok(token),
831            None => Err(ApiError::TokenRequest(format!(
832                "Failed to get token: {}",
833                response.response_msg
834            ))),
835        }
836    }
837
838    /// Verify a token for subject doing operation on resource.
839    /// This will verify the token using the remote authorization service API.
840    pub async fn verify_token(
841        &self,
842        token: String,
843        subject: String,
844        resource: String,
845        operation: String,
846    ) -> Result<String, ApiError> {
847        let request = VerifyTokenRequest {
848            token,
849            subject,
850            resource,
851            operation,
852        };
853
854        let response = match self {
855            HessraClient::Http1(client) => {
856                client
857                    .send_request::<_, VerifyTokenResponse>("verify_token", &request)
858                    .await?
859            }
860            #[cfg(feature = "http3")]
861            HessraClient::Http3(client) => {
862                client
863                    .send_request::<_, VerifyTokenResponse>("verify_token", &request)
864                    .await?
865            }
866        };
867
868        Ok(response.response_msg)
869    }
870
871    /// Verify a service chain token. If no component is provided,
872    /// the entire service chain will be used to verify the token.
873    /// If a component name is provided, the service chain up to and
874    /// excluding the component will be used to verify the token. This
875    /// is useful for a node in the middle of the service chain
876    /// verifying a token has been attested by all previous nodes.
877    pub async fn verify_service_chain_token(
878        &self,
879        token: String,
880        subject: String,
881        resource: String,
882        component: Option<String>,
883    ) -> Result<String, ApiError> {
884        let request = VerifyServiceChainTokenRequest {
885            token,
886            subject,
887            resource,
888            component,
889        };
890
891        let response = match self {
892            HessraClient::Http1(client) => {
893                client
894                    .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
895                    .await?
896            }
897            #[cfg(feature = "http3")]
898            HessraClient::Http3(client) => {
899                client
900                    .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
901                    .await?
902            }
903        };
904
905        Ok(response.response_msg)
906    }
907
908    /// Sign a multi-party token by calling an authorization service's signoff endpoint
909    pub async fn sign_token(
910        &self,
911        token: &str,
912        resource: &str,
913        operation: &str,
914    ) -> Result<SignTokenResponse, ApiError> {
915        let request = SignTokenRequest {
916            token: token.to_string(),
917            resource: resource.to_string(),
918            operation: operation.to_string(),
919        };
920
921        let response = match self {
922            HessraClient::Http1(client) => {
923                client
924                    .send_request::<_, SignTokenResponse>("sign_token", &request)
925                    .await?
926            }
927            #[cfg(feature = "http3")]
928            HessraClient::Http3(client) => {
929                client
930                    .send_request::<_, SignTokenResponse>("sign_token", &request)
931                    .await?
932            }
933        };
934
935        Ok(response)
936    }
937
938    /// Get the public key from the server
939    pub async fn get_public_key(&self) -> Result<String, ApiError> {
940        let url_path = "public_key";
941
942        let response = match self {
943            HessraClient::Http1(client) => {
944                // For this endpoint, we just need a GET request, not a POST with a body
945                let base_url = client.config.get_base_url();
946                let full_url = format!("https://{base_url}/{url_path}");
947
948                let response = client
949                    .client
950                    .get(&full_url)
951                    .send()
952                    .await
953                    .map_err(ApiError::HttpClient)?;
954
955                if !response.status().is_success() {
956                    let status = response.status();
957                    let error_text = response.text().await.unwrap_or_default();
958                    return Err(ApiError::InvalidResponse(format!(
959                        "HTTP error: {status} - {error_text}"
960                    )));
961                }
962
963                response.json::<PublicKeyResponse>().await.map_err(|e| {
964                    ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
965                })?
966            }
967            #[cfg(feature = "http3")]
968            HessraClient::Http3(client) => {
969                let base_url = client.config.get_base_url();
970                let full_url = format!("https://{base_url}/{url_path}");
971
972                let response = client
973                    .client
974                    .get(&full_url)
975                    .send()
976                    .await
977                    .map_err(ApiError::HttpClient)?;
978
979                if !response.status().is_success() {
980                    let status = response.status();
981                    let error_text = response.text().await.unwrap_or_default();
982                    return Err(ApiError::InvalidResponse(format!(
983                        "HTTP error: {status} - {error_text}"
984                    )));
985                }
986
987                response.json::<PublicKeyResponse>().await.map_err(|e| {
988                    ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
989                })?
990            }
991        };
992
993        Ok(response.public_key)
994    }
995
996    /// Request a new identity token from the authorization service
997    ///
998    /// This endpoint requires mTLS authentication as it's the initial issuance of an identity token.
999    /// The identifier parameter is optional when using mTLS, as the identity can be derived from the client certificate.
1000    ///
1001    /// # Arguments
1002    /// * `identifier` - Optional identifier for the identity. Required for non-mTLS future requests, optional with mTLS.
1003    pub async fn request_identity_token(
1004        &self,
1005        identifier: Option<String>,
1006    ) -> Result<IdentityTokenResponse, ApiError> {
1007        let request = IdentityTokenRequest { identifier };
1008
1009        let response = match self {
1010            HessraClient::Http1(client) => {
1011                client
1012                    .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
1013                    .await?
1014            }
1015            #[cfg(feature = "http3")]
1016            HessraClient::Http3(client) => {
1017                client
1018                    .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
1019                    .await?
1020            }
1021        };
1022
1023        Ok(response)
1024    }
1025
1026    /// Refresh an existing identity token
1027    ///
1028    /// This endpoint can use either mTLS or the current identity token for authentication.
1029    /// When using identity token authentication (no mTLS), the identifier parameter is required.
1030    /// The current token will be validated and a new token with updated expiration will be issued.
1031    ///
1032    /// # Arguments
1033    /// * `current_token` - The existing identity token to refresh
1034    /// * `identifier` - Optional identifier. Required when not using mTLS authentication.
1035    pub async fn refresh_identity_token(
1036        &self,
1037        current_token: String,
1038        identifier: Option<String>,
1039    ) -> Result<IdentityTokenResponse, ApiError> {
1040        let request = RefreshIdentityTokenRequest {
1041            current_token,
1042            identifier,
1043        };
1044
1045        let response = match self {
1046            HessraClient::Http1(client) => {
1047                client
1048                    .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
1049                    .await?
1050            }
1051            #[cfg(feature = "http3")]
1052            HessraClient::Http3(client) => {
1053                client
1054                    .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
1055                    .await?
1056            }
1057        };
1058
1059        Ok(response)
1060    }
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065    use super::*;
1066
1067    // Test BaseConfig get_base_url method
1068    #[test]
1069    fn test_base_config_get_base_url_with_port() {
1070        let config = BaseConfig {
1071            base_url: "test.hessra.net".to_string(),
1072            port: Some(443),
1073            mtls_key: None,
1074            mtls_cert: None,
1075            server_ca: "".to_string(),
1076            public_key: None,
1077            personal_keypair: None,
1078        };
1079
1080        assert_eq!(config.get_base_url(), "test.hessra.net:443");
1081    }
1082
1083    #[test]
1084    fn test_base_config_get_base_url_without_port() {
1085        let config = BaseConfig {
1086            base_url: "test.hessra.net".to_string(),
1087            port: None,
1088            mtls_key: None,
1089            mtls_cert: None,
1090            server_ca: "".to_string(),
1091            public_key: None,
1092            personal_keypair: None,
1093        };
1094
1095        assert_eq!(config.get_base_url(), "test.hessra.net");
1096    }
1097
1098    // Test HessraClientBuilder methods
1099    #[test]
1100    fn test_client_builder_methods() {
1101        let builder = HessraClientBuilder::new()
1102            .base_url("test.hessra.net")
1103            .port(443)
1104            .protocol(Protocol::Http1)
1105            .mtls_cert("CERT")
1106            .mtls_key("KEY")
1107            .server_ca("CA")
1108            .public_key("PUBKEY")
1109            .personal_keypair("KEYPAIR");
1110
1111        assert_eq!(builder.config.base_url, "test.hessra.net");
1112        assert_eq!(builder.config.port, Some(443));
1113        assert_eq!(builder.config.mtls_cert, Some("CERT".to_string()));
1114        assert_eq!(builder.config.mtls_key, Some("KEY".to_string()));
1115        assert_eq!(builder.config.server_ca, "CA");
1116        assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
1117        assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
1118    }
1119}