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/// Request payload for verifying a service chain token
133#[derive(Serialize, Deserialize)]
134pub struct VerifyServiceChainTokenRequest {
135    pub token: String,
136    pub subject: String,
137    pub resource: String,
138    pub component: Option<String>,
139}
140
141/// Request for minting a new identity token
142#[derive(Serialize, Deserialize)]
143pub struct IdentityTokenRequest {
144    /// Optional identifier - required for token-only auth, optional for mTLS
145    pub identifier: Option<String>,
146}
147
148/// Request for refreshing an existing identity token
149#[derive(Serialize, Deserialize)]
150pub struct RefreshIdentityTokenRequest {
151    /// The current identity token to refresh
152    pub current_token: String,
153    /// Optional identifier - required for token-only auth, optional for mTLS
154    pub identifier: Option<String>,
155}
156
157/// Response from identity token operations
158#[derive(Serialize, Deserialize, Debug, Clone)]
159pub struct IdentityTokenResponse {
160    /// Response message from the server
161    pub response_msg: String,
162    /// The issued identity token, if successful
163    pub token: Option<String>,
164    /// Time until expiration in seconds
165    pub expires_in: Option<u64>,
166    /// The identity contained in the token
167    pub identity: Option<String>,
168}
169
170/// Base configuration for Hessra clients
171#[derive(Clone)]
172pub struct BaseConfig {
173    /// Base URL of the Hessra service (without protocol scheme)
174    pub base_url: String,
175    /// Optional port to connect to
176    pub port: Option<u16>,
177    /// Optional mTLS private key in PEM format (required for mTLS auth)
178    pub mtls_key: Option<String>,
179    /// Optional mTLS client certificate in PEM format (required for mTLS auth)
180    pub mtls_cert: Option<String>,
181    /// Server CA certificate in PEM format
182    pub server_ca: String,
183    /// Public key for token verification in PEM format
184    pub public_key: Option<String>,
185    /// Personal keypair for service chain attestation
186    pub personal_keypair: Option<String>,
187}
188
189impl BaseConfig {
190    /// Get the formatted base URL, with port if specified
191    pub fn get_base_url(&self) -> String {
192        match self.port {
193            Some(port) => format!("{}:{port}", self.base_url),
194            None => self.base_url.clone(),
195        }
196    }
197}
198
199/// HTTP/1.1 client implementation
200pub struct Http1Client {
201    /// Base configuration
202    config: BaseConfig,
203    /// reqwest HTTP client with mTLS configured
204    client: reqwest::Client,
205}
206
207impl Http1Client {
208    /// Create a new HTTP/1.1 client with the given configuration
209    pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
210        // Parse the CA certificate (always required for server verification)
211        let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
212            .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {e}")))?;
213
214        // Build the client with or without mTLS depending on configuration
215        let mut client_builder = reqwest::ClientBuilder::new()
216            .use_rustls_tls()
217            .add_root_certificate(cert_der);
218
219        // Add mTLS identity if both cert and key are provided
220        if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
221            let identity_str = format!("{cert}{key}");
222            let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
223                ApiError::SslConfig(format!(
224                    "Failed to create identity from certificate and key: {e}"
225                ))
226            })?;
227            client_builder = client_builder.identity(identity);
228        }
229
230        let client = client_builder
231            .build()
232            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
233
234        Ok(Self { config, client })
235    }
236
237    /// Send a request to the remote Hessra authorization service
238    pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
239    where
240        T: Serialize,
241        R: for<'de> Deserialize<'de>,
242    {
243        let base_url = self.config.get_base_url();
244        let url = format!("https://{base_url}/{endpoint}");
245
246        let response = self
247            .client
248            .post(&url)
249            .json(request_body)
250            .send()
251            .await
252            .map_err(ApiError::HttpClient)?;
253
254        if !response.status().is_success() {
255            let status = response.status();
256            let error_text = response.text().await.unwrap_or_default();
257            return Err(ApiError::InvalidResponse(format!(
258                "HTTP error: {status} - {error_text}"
259            )));
260        }
261
262        let result = response
263            .json::<R>()
264            .await
265            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
266
267        Ok(result)
268    }
269
270    pub async fn send_request_with_auth<T, R>(
271        &self,
272        endpoint: &str,
273        request_body: &T,
274        auth_header: &str,
275    ) -> Result<R, ApiError>
276    where
277        T: Serialize,
278        R: for<'de> Deserialize<'de>,
279    {
280        let base_url = self.config.get_base_url();
281        let url = format!("https://{base_url}/{endpoint}");
282
283        let response = self
284            .client
285            .post(&url)
286            .header("Authorization", auth_header)
287            .json(request_body)
288            .send()
289            .await
290            .map_err(ApiError::HttpClient)?;
291
292        if !response.status().is_success() {
293            let status = response.status();
294            let error_text = response.text().await.unwrap_or_default();
295            return Err(ApiError::InvalidResponse(format!(
296                "HTTP error: {status} - {error_text}"
297            )));
298        }
299
300        let result = response
301            .json::<R>()
302            .await
303            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
304
305        Ok(result)
306    }
307}
308
309/// HTTP/3 client implementation (only available with the "http3" feature)
310#[cfg(feature = "http3")]
311pub struct Http3Client {
312    /// Base configuration
313    config: BaseConfig,
314    /// QUIC endpoint for HTTP/3 connections
315    client: reqwest::Client,
316}
317
318#[cfg(feature = "http3")]
319impl Http3Client {
320    /// Create a new HTTP/3 client with the given configuration
321    pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
322        // Parse the CA certificate (always required for server verification)
323        let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
324            .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {e}")))?;
325
326        // Build the client with or without mTLS depending on configuration
327        let mut client_builder = reqwest::ClientBuilder::new()
328            .use_rustls_tls()
329            .http3_prior_knowledge()
330            .add_root_certificate(cert_der);
331
332        // Add mTLS identity if both cert and key are provided
333        if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
334            let identity_str = format!("{}{}", cert, key);
335            let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
336                ApiError::SslConfig(format!(
337                    "Failed to create identity from certificate and key: {e}"
338                ))
339            })?;
340            client_builder = client_builder.identity(identity);
341        }
342
343        let client = client_builder
344            .build()
345            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
346
347        Ok(Self { config, client })
348    }
349
350    /// Send a request to the Hessra service
351    pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
352    where
353        T: Serialize,
354        R: for<'de> Deserialize<'de>,
355    {
356        let base_url = self.config.get_base_url();
357        let url = format!("https://{base_url}/{endpoint}");
358
359        let response = self
360            .client
361            .post(&url)
362            .json(request_body)
363            .send()
364            .await
365            .map_err(ApiError::HttpClient)?;
366
367        if !response.status().is_success() {
368            let status = response.status();
369            let error_text = response.text().await.unwrap_or_default();
370            return Err(ApiError::InvalidResponse(format!(
371                "HTTP error: {status} - {error_text}"
372            )));
373        }
374
375        let result = response
376            .json::<R>()
377            .await
378            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
379
380        Ok(result)
381    }
382
383    pub async fn send_request_with_auth<T, R>(
384        &self,
385        endpoint: &str,
386        request_body: &T,
387        auth_header: &str,
388    ) -> Result<R, ApiError>
389    where
390        T: Serialize,
391        R: for<'de> Deserialize<'de>,
392    {
393        let base_url = self.config.get_base_url();
394        let url = format!("https://{base_url}/{endpoint}");
395
396        let response = self
397            .client
398            .post(&url)
399            .header("Authorization", auth_header)
400            .json(request_body)
401            .send()
402            .await
403            .map_err(ApiError::HttpClient)?;
404
405        if !response.status().is_success() {
406            let status = response.status();
407            let error_text = response.text().await.unwrap_or_default();
408            return Err(ApiError::InvalidResponse(format!(
409                "HTTP error: {status} - {error_text}"
410            )));
411        }
412
413        let result = response
414            .json::<R>()
415            .await
416            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
417
418        Ok(result)
419    }
420}
421
422/// The main Hessra client type providing token request and verification
423pub enum HessraClient {
424    /// HTTP/1.1 client
425    Http1(Http1Client),
426    /// HTTP/3 client (only available with the "http3" feature)
427    #[cfg(feature = "http3")]
428    Http3(Http3Client),
429}
430
431/// Builder for creating Hessra clients
432pub struct HessraClientBuilder {
433    /// Base configuration being built
434    config: BaseConfig,
435    /// Protocol to use for the client
436    protocol: hessra_config::Protocol,
437}
438
439impl HessraClientBuilder {
440    /// Create a new client builder with default values
441    pub fn new() -> Self {
442        Self {
443            config: BaseConfig {
444                base_url: String::new(),
445                port: None,
446                mtls_key: None,
447                mtls_cert: None,
448                server_ca: String::new(),
449                public_key: None,
450                personal_keypair: None,
451            },
452            protocol: Protocol::Http1,
453        }
454    }
455
456    /// Create a client builder from a HessraConfig
457    pub fn from_config(mut self, config: &HessraConfig) -> Self {
458        self.config.base_url = config.base_url.clone();
459        self.config.port = config.port;
460        self.config.mtls_key = config.mtls_key.clone();
461        self.config.mtls_cert = config.mtls_cert.clone();
462        self.config.server_ca = config.server_ca.clone();
463        self.config.public_key = config.public_key.clone();
464        self.config.personal_keypair = config.personal_keypair.clone();
465        self.protocol = config.protocol.clone();
466        self
467    }
468
469    /// Set the base URL for the client, e.g. "test.hessra.net"
470    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
471        self.config.base_url = base_url.into();
472        self
473    }
474
475    /// Set the mTLS private key for the client
476    /// PEM formatted string
477    pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
478        self.config.mtls_key = Some(mtls_key.into());
479        self
480    }
481
482    /// Set the mTLS certificate for the client
483    /// PEM formatted string
484    pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
485        self.config.mtls_cert = Some(mtls_cert.into());
486        self
487    }
488
489    /// Set the server CA certificate for the client
490    /// PEM formatted string
491    pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
492        self.config.server_ca = server_ca.into();
493        self
494    }
495
496    /// Set the port for the client
497    pub fn port(mut self, port: u16) -> Self {
498        self.config.port = Some(port);
499        self
500    }
501
502    /// Set the protocol for the client
503    pub fn protocol(mut self, protocol: Protocol) -> Self {
504        self.protocol = protocol;
505        self
506    }
507
508    /// Set the public key for token verification
509    /// PEM formatted string. note, this is JUST the public key, not the entire keypair.
510    pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
511        self.config.public_key = Some(public_key.into());
512        self
513    }
514
515    /// Set the personal keypair for service chain attestation
516    /// PEM formatted string. note, this is the entire keypair
517    /// and needs to be kept secret.
518    pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
519        self.config.personal_keypair = Some(keypair.into());
520        self
521    }
522
523    /// Build the HTTP/1.1 client
524    fn build_http1(&self) -> Result<Http1Client, ApiError> {
525        Http1Client::new(self.config.clone())
526    }
527
528    /// Build the HTTP/3 client
529    #[cfg(feature = "http3")]
530    fn build_http3(&self) -> Result<Http3Client, ApiError> {
531        Http3Client::new(self.config.clone())
532    }
533
534    /// Build the client
535    pub fn build(self) -> Result<HessraClient, ApiError> {
536        match self.protocol {
537            Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
538            #[cfg(feature = "http3")]
539            Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
540            #[allow(unreachable_patterns)]
541            _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
542        }
543    }
544}
545
546impl Default for HessraClientBuilder {
547    fn default() -> Self {
548        Self::new()
549    }
550}
551
552impl HessraClient {
553    /// Create a new client builder
554    pub fn builder() -> HessraClientBuilder {
555        HessraClientBuilder::new()
556    }
557
558    /// Fetch the public key from the Hessra service without creating a client
559    /// The public_key endpoint is available as both an authenticated and unauthenticated
560    /// request.
561    pub async fn fetch_public_key(
562        base_url: impl Into<String>,
563        port: Option<u16>,
564        server_ca: impl Into<String>,
565    ) -> Result<String, ApiError> {
566        let base_url = base_url.into();
567        let server_ca = server_ca.into();
568
569        // Create a regular reqwest client (no mTLS)
570        let cert_pem = server_ca.as_bytes();
571        let cert_der = reqwest::Certificate::from_pem(cert_pem)
572            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
573
574        let client = reqwest::ClientBuilder::new()
575            .use_rustls_tls()
576            .add_root_certificate(cert_der)
577            .build()
578            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
579
580        // Format the URL
581        let url = match port {
582            Some(port) => format!("https://{base_url}:{port}/public_key"),
583            None => format!("https://{base_url}/public_key"),
584        };
585
586        // Make the request
587        let response = client
588            .get(&url)
589            .send()
590            .await
591            .map_err(ApiError::HttpClient)?;
592
593        if !response.status().is_success() {
594            let status = response.status();
595            let error_text = response.text().await.unwrap_or_default();
596            return Err(ApiError::InvalidResponse(format!(
597                "HTTP error: {status} - {error_text}"
598            )));
599        }
600
601        // Parse the response
602        let result = response
603            .json::<PublicKeyResponse>()
604            .await
605            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
606
607        Ok(result.public_key)
608    }
609
610    #[cfg(feature = "http3")]
611    pub async fn fetch_public_key_http3(
612        base_url: impl Into<String>,
613        port: Option<u16>,
614        server_ca: impl Into<String>,
615    ) -> Result<String, ApiError> {
616        let base_url = base_url.into();
617        let server_ca = server_ca.into();
618
619        // Create a regular reqwest client (no mTLS)
620        let cert_pem = server_ca.as_bytes();
621        let cert_der = reqwest::Certificate::from_pem(cert_pem)
622            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
623
624        let client = reqwest::ClientBuilder::new()
625            .use_rustls_tls()
626            .add_root_certificate(cert_der)
627            .http3_prior_knowledge()
628            .build()
629            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
630
631        // Format the URL
632        let url = match port {
633            Some(port) => format!("https://{base_url}:{port}/public_key"),
634            None => format!("https://{base_url}/public_key"),
635        };
636
637        // Make the request
638        let response = client
639            .get(&url)
640            .send()
641            .await
642            .map_err(ApiError::HttpClient)?;
643
644        if !response.status().is_success() {
645            let status = response.status();
646            let error_text = response.text().await.unwrap_or_default();
647            return Err(ApiError::InvalidResponse(format!(
648                "HTTP error: {status} - {error_text}"
649            )));
650        }
651
652        // Parse the response
653        let result = response
654            .json::<PublicKeyResponse>()
655            .await
656            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
657
658        Ok(result.public_key)
659    }
660
661    /// Request a token for a resource
662    /// Returns the full TokenResponse which may include pending signoffs for multi-party tokens
663    pub async fn request_token(
664        &self,
665        resource: String,
666        operation: String,
667    ) -> Result<TokenResponse, ApiError> {
668        let request = TokenRequest {
669            resource,
670            operation,
671        };
672
673        let response = match self {
674            HessraClient::Http1(client) => {
675                client
676                    .send_request::<_, TokenResponse>("request_token", &request)
677                    .await?
678            }
679            #[cfg(feature = "http3")]
680            HessraClient::Http3(client) => {
681                client
682                    .send_request::<_, TokenResponse>("request_token", &request)
683                    .await?
684            }
685        };
686
687        Ok(response)
688    }
689
690    /// Request a token for a resource using an identity token for authentication
691    /// The identity token will be sent in the Authorization header as a Bearer token
692    /// Returns the full TokenResponse which may include pending signoffs for multi-party tokens
693    pub async fn request_token_with_identity(
694        &self,
695        resource: String,
696        operation: String,
697        identity_token: String,
698    ) -> Result<TokenResponse, ApiError> {
699        let request = TokenRequest {
700            resource,
701            operation,
702        };
703
704        let response = match self {
705            HessraClient::Http1(client) => {
706                client
707                    .send_request_with_auth::<_, TokenResponse>(
708                        "request_token",
709                        &request,
710                        &format!("Bearer {identity_token}"),
711                    )
712                    .await?
713            }
714            #[cfg(feature = "http3")]
715            HessraClient::Http3(client) => {
716                client
717                    .send_request_with_auth::<_, TokenResponse>(
718                        "request_token",
719                        &request,
720                        &format!("Bearer {identity_token}"),
721                    )
722                    .await?
723            }
724        };
725
726        Ok(response)
727    }
728
729    /// Request a token for a resource (legacy method)
730    /// This method returns just the token string for backward compatibility
731    pub async fn request_token_simple(
732        &self,
733        resource: String,
734        operation: String,
735    ) -> Result<String, ApiError> {
736        let response = self.request_token(resource, operation).await?;
737
738        match response.token {
739            Some(token) => Ok(token),
740            None => Err(ApiError::TokenRequest(format!(
741                "Failed to get token: {}",
742                response.response_msg
743            ))),
744        }
745    }
746
747    /// Verify a token for subject doing operation on resource.
748    /// This will verify the token using the remote authorization service API.
749    pub async fn verify_token(
750        &self,
751        token: String,
752        subject: String,
753        resource: String,
754        operation: String,
755    ) -> Result<String, ApiError> {
756        let request = VerifyTokenRequest {
757            token,
758            subject,
759            resource,
760            operation,
761        };
762
763        let response = match self {
764            HessraClient::Http1(client) => {
765                client
766                    .send_request::<_, VerifyTokenResponse>("verify_token", &request)
767                    .await?
768            }
769            #[cfg(feature = "http3")]
770            HessraClient::Http3(client) => {
771                client
772                    .send_request::<_, VerifyTokenResponse>("verify_token", &request)
773                    .await?
774            }
775        };
776
777        Ok(response.response_msg)
778    }
779
780    /// Verify a service chain token. If no component is provided,
781    /// the entire service chain will be used to verify the token.
782    /// If a component name is provided, the service chain up to and
783    /// excluding the component will be used to verify the token. This
784    /// is useful for a node in the middle of the service chain
785    /// verifying a token has been attested by all previous nodes.
786    pub async fn verify_service_chain_token(
787        &self,
788        token: String,
789        subject: String,
790        resource: String,
791        component: Option<String>,
792    ) -> Result<String, ApiError> {
793        let request = VerifyServiceChainTokenRequest {
794            token,
795            subject,
796            resource,
797            component,
798        };
799
800        let response = match self {
801            HessraClient::Http1(client) => {
802                client
803                    .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
804                    .await?
805            }
806            #[cfg(feature = "http3")]
807            HessraClient::Http3(client) => {
808                client
809                    .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
810                    .await?
811            }
812        };
813
814        Ok(response.response_msg)
815    }
816
817    /// Sign a multi-party token by calling an authorization service's signoff endpoint
818    pub async fn sign_token(
819        &self,
820        token: &str,
821        resource: &str,
822        operation: &str,
823    ) -> Result<SignTokenResponse, ApiError> {
824        let request = SignTokenRequest {
825            token: token.to_string(),
826            resource: resource.to_string(),
827            operation: operation.to_string(),
828        };
829
830        let response = match self {
831            HessraClient::Http1(client) => {
832                client
833                    .send_request::<_, SignTokenResponse>("sign_token", &request)
834                    .await?
835            }
836            #[cfg(feature = "http3")]
837            HessraClient::Http3(client) => {
838                client
839                    .send_request::<_, SignTokenResponse>("sign_token", &request)
840                    .await?
841            }
842        };
843
844        Ok(response)
845    }
846
847    /// Get the public key from the server
848    pub async fn get_public_key(&self) -> Result<String, ApiError> {
849        let url_path = "public_key";
850
851        let response = match self {
852            HessraClient::Http1(client) => {
853                // For this endpoint, we just need a GET request, not a POST with a body
854                let base_url = client.config.get_base_url();
855                let full_url = format!("https://{base_url}/{url_path}");
856
857                let response = client
858                    .client
859                    .get(&full_url)
860                    .send()
861                    .await
862                    .map_err(ApiError::HttpClient)?;
863
864                if !response.status().is_success() {
865                    let status = response.status();
866                    let error_text = response.text().await.unwrap_or_default();
867                    return Err(ApiError::InvalidResponse(format!(
868                        "HTTP error: {status} - {error_text}"
869                    )));
870                }
871
872                response.json::<PublicKeyResponse>().await.map_err(|e| {
873                    ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
874                })?
875            }
876            #[cfg(feature = "http3")]
877            HessraClient::Http3(client) => {
878                let base_url = client.config.get_base_url();
879                let full_url = format!("https://{base_url}/{url_path}");
880
881                let response = client
882                    .client
883                    .get(&full_url)
884                    .send()
885                    .await
886                    .map_err(ApiError::HttpClient)?;
887
888                if !response.status().is_success() {
889                    let status = response.status();
890                    let error_text = response.text().await.unwrap_or_default();
891                    return Err(ApiError::InvalidResponse(format!(
892                        "HTTP error: {status} - {error_text}"
893                    )));
894                }
895
896                response.json::<PublicKeyResponse>().await.map_err(|e| {
897                    ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
898                })?
899            }
900        };
901
902        Ok(response.public_key)
903    }
904
905    /// Request a new identity token from the authorization service
906    ///
907    /// This endpoint requires mTLS authentication as it's the initial issuance of an identity token.
908    /// The identifier parameter is optional when using mTLS, as the identity can be derived from the client certificate.
909    ///
910    /// # Arguments
911    /// * `identifier` - Optional identifier for the identity. Required for non-mTLS future requests, optional with mTLS.
912    pub async fn request_identity_token(
913        &self,
914        identifier: Option<String>,
915    ) -> Result<IdentityTokenResponse, ApiError> {
916        let request = IdentityTokenRequest { identifier };
917
918        let response = match self {
919            HessraClient::Http1(client) => {
920                client
921                    .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
922                    .await?
923            }
924            #[cfg(feature = "http3")]
925            HessraClient::Http3(client) => {
926                client
927                    .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
928                    .await?
929            }
930        };
931
932        Ok(response)
933    }
934
935    /// Refresh an existing identity token
936    ///
937    /// This endpoint can use either mTLS or the current identity token for authentication.
938    /// When using identity token authentication (no mTLS), the identifier parameter is required.
939    /// The current token will be validated and a new token with updated expiration will be issued.
940    ///
941    /// # Arguments
942    /// * `current_token` - The existing identity token to refresh
943    /// * `identifier` - Optional identifier. Required when not using mTLS authentication.
944    pub async fn refresh_identity_token(
945        &self,
946        current_token: String,
947        identifier: Option<String>,
948    ) -> Result<IdentityTokenResponse, ApiError> {
949        let request = RefreshIdentityTokenRequest {
950            current_token,
951            identifier,
952        };
953
954        let response = match self {
955            HessraClient::Http1(client) => {
956                client
957                    .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
958                    .await?
959            }
960            #[cfg(feature = "http3")]
961            HessraClient::Http3(client) => {
962                client
963                    .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
964                    .await?
965            }
966        };
967
968        Ok(response)
969    }
970}
971
972#[cfg(test)]
973mod tests {
974    use super::*;
975
976    // Test BaseConfig get_base_url method
977    #[test]
978    fn test_base_config_get_base_url_with_port() {
979        let config = BaseConfig {
980            base_url: "test.hessra.net".to_string(),
981            port: Some(443),
982            mtls_key: None,
983            mtls_cert: None,
984            server_ca: "".to_string(),
985            public_key: None,
986            personal_keypair: None,
987        };
988
989        assert_eq!(config.get_base_url(), "test.hessra.net:443");
990    }
991
992    #[test]
993    fn test_base_config_get_base_url_without_port() {
994        let config = BaseConfig {
995            base_url: "test.hessra.net".to_string(),
996            port: None,
997            mtls_key: None,
998            mtls_cert: None,
999            server_ca: "".to_string(),
1000            public_key: None,
1001            personal_keypair: None,
1002        };
1003
1004        assert_eq!(config.get_base_url(), "test.hessra.net");
1005    }
1006
1007    // Test HessraClientBuilder methods
1008    #[test]
1009    fn test_client_builder_methods() {
1010        let builder = HessraClientBuilder::new()
1011            .base_url("test.hessra.net")
1012            .port(443)
1013            .protocol(Protocol::Http1)
1014            .mtls_cert("CERT")
1015            .mtls_key("KEY")
1016            .server_ca("CA")
1017            .public_key("PUBKEY")
1018            .personal_keypair("KEYPAIR");
1019
1020        assert_eq!(builder.config.base_url, "test.hessra.net");
1021        assert_eq!(builder.config.port, Some(443));
1022        assert_eq!(builder.config.mtls_cert, Some("CERT".to_string()));
1023        assert_eq!(builder.config.mtls_key, Some("KEY".to_string()));
1024        assert_eq!(builder.config.server_ca, "CA");
1025        assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
1026        assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
1027    }
1028}