Skip to main content

aep_service/
hosted.rs

1use std::{sync::Arc, time::Duration};
2
3use aep_core::{
4    ClientAssertionClaims, HttpRequest, HttpTransport, decode_jwt_unverified,
5    parse_client_assertion_claims,
6};
7use async_trait::async_trait;
8use futures::future::{Either, select};
9use http::{HeaderMap, HeaderValue, Method, header};
10use serde::Deserialize;
11use serde_json::json;
12use sha2::{Digest as _, Sha256};
13use url::Url;
14
15use crate::{
16    ClientAssertionVerificationContext, ClientAssertionVerifier, ReqwestTransport, ServiceError,
17};
18
19#[derive(Clone)]
20pub struct HostedPlatformClientAssertionVerifierOptions {
21    pub authorization: Option<String>,
22    pub endpoint: String,
23    pub maximum_response_bytes: usize,
24    pub request_timeout: Duration,
25    pub transport: Option<Arc<dyn HttpTransport>>,
26}
27
28impl HostedPlatformClientAssertionVerifierOptions {
29    pub fn new(endpoint: impl Into<String>) -> Self {
30        Self {
31            authorization: None,
32            endpoint: endpoint.into(),
33            maximum_response_bytes: 1 << 20,
34            request_timeout: Duration::from_secs(30),
35            transport: None,
36        }
37    }
38}
39
40pub struct HostedPlatformClientAssertionVerifier {
41    authorization: Option<HeaderValue>,
42    endpoint: Url,
43    maximum_response_bytes: usize,
44    request_timeout: Duration,
45    transport: Arc<dyn HttpTransport>,
46}
47
48impl HostedPlatformClientAssertionVerifier {
49    pub fn new(
50        options: HostedPlatformClientAssertionVerifierOptions,
51    ) -> Result<Self, ServiceError> {
52        let endpoint = Url::parse(&options.endpoint).map_err(|_| ServiceError::InvalidConfiguration(
53            "AEP hosted verification requires an HTTPS endpoint without credentials or a fragment".into()))?;
54        if endpoint.scheme() != "https"
55            || endpoint.host_str().is_none()
56            || !endpoint.username().is_empty()
57            || endpoint.password().is_some()
58            || endpoint.fragment().is_some()
59        {
60            return Err(ServiceError::InvalidConfiguration(
61                "AEP hosted verification requires an HTTPS endpoint without credentials or a fragment".into()));
62        }
63        if options.maximum_response_bytes == 0 || options.request_timeout.is_zero() {
64            return Err(ServiceError::InvalidConfiguration(
65                "AEP hosted verification response limit and timeout must be positive".into(),
66            ));
67        }
68        let authorization = options
69            .authorization
70            .as_deref()
71            .map(|value| {
72                let mut value = HeaderValue::from_str(value).map_err(|_| {
73                    ServiceError::InvalidConfiguration(
74                        "AEP hosted verification authorization is not a valid HTTP field value"
75                            .into(),
76                    )
77                })?;
78                value.set_sensitive(true);
79                Ok::<_, ServiceError>(value)
80            })
81            .transpose()?;
82        let transport = match options.transport {
83            Some(transport) => transport,
84            None => Arc::new(
85                ReqwestTransport::new(options.maximum_response_bytes, options.request_timeout)
86                    .map_err(|_| {
87                        ServiceError::InvalidConfiguration(
88                            "AEP hosted verification HTTP client could not be created".into(),
89                        )
90                    })?,
91            ),
92        };
93        Ok(Self {
94            authorization,
95            endpoint,
96            maximum_response_bytes: options.maximum_response_bytes,
97            request_timeout: options.request_timeout,
98            transport,
99        })
100    }
101}
102
103#[derive(Deserialize)]
104struct VerificationResponse {
105    agent_did: Option<String>,
106    op: Option<aep_core::AssertionOperation>,
107    #[serde(rename = "reason")]
108    _reason: String,
109    service_did: String,
110    verified: bool,
111}
112
113#[async_trait]
114impl ClientAssertionVerifier for HostedPlatformClientAssertionVerifier {
115    async fn verify(
116        &self,
117        context: ClientAssertionVerificationContext,
118    ) -> Result<ClientAssertionClaims, ServiceError> {
119        let decoded = decode_jwt_unverified(&context.assertion)?;
120        if !context
121            .signing_algorithms
122            .contains(&decoded.header.algorithm)
123        {
124            return Err(ServiceError::Handler(
125                "AEP client assertion algorithm was not advertised by the Service".into(),
126            ));
127        }
128        let claims = parse_client_assertion_claims(&serde_json::to_vec(&decoded.payload)?)
129            .map_err(|_| ServiceError::Handler("AEP client assertion claims are invalid".into()))?;
130        let mut body = json!({"client_assertion":context.assertion,"op":context.operation,"service_did":context.service_did});
131        if let Some(resource) = &context.resource {
132            body["resource"] = json!(resource.as_str());
133        }
134        let body = serde_json::to_vec(&body)?;
135        // Command retries use fresh assertions; the key identifies this exact verification body.
136        let key: String = Sha256::digest(&body)
137            .iter()
138            .map(|byte| format!("{byte:02x}"))
139            .collect();
140        let mut headers = HeaderMap::from_iter([
141            (
142                header::ACCEPT,
143                HeaderValue::from_static(aep_core::MEDIA_TYPE),
144            ),
145            (
146                header::CONTENT_TYPE,
147                HeaderValue::from_static(aep_core::MEDIA_TYPE),
148            ),
149            (
150                http::HeaderName::from_static("idempotency-key"),
151                HeaderValue::from_str(&key).map_err(|_| {
152                    ServiceError::Handler("AEP hosted verification key is invalid".into())
153                })?,
154            ),
155        ]);
156        if let Some(authorization) = &self.authorization {
157            headers.insert(header::AUTHORIZATION, authorization.clone());
158        }
159        let request = HttpRequest {
160            method: Method::POST,
161            url: self.endpoint.clone(),
162            headers,
163            body,
164        };
165        let response = match select(
166            self.transport.send(request),
167            Box::pin(futures_timer::Delay::new(self.request_timeout)),
168        )
169        .await
170        {
171            Either::Left((result, _)) => result.map_err(|_| {
172                ServiceError::Handler("AEP hosted verification request failed".into())
173            })?,
174            Either::Right(_) => {
175                return Err(ServiceError::Handler(
176                    "AEP hosted verification request timed out".into(),
177                ));
178            }
179        };
180        if response.final_url != self.endpoint {
181            return Err(ServiceError::Handler(
182                "AEP hosted verification redirects are not allowed".into(),
183            ));
184        }
185        if !response.status.is_success() {
186            return Err(ServiceError::Handler(format!(
187                "AEP hosted verification failed with HTTP {}",
188                response.status.as_u16()
189            )));
190        }
191        if response.body.len() > self.maximum_response_bytes {
192            return Err(ServiceError::Handler(
193                "AEP hosted verification response exceeds the configured limit".into(),
194            ));
195        }
196        let result: VerificationResponse =
197            serde_json::from_slice(&response.body).map_err(|_| {
198                ServiceError::Handler("AEP hosted verification response is invalid".into())
199            })?;
200        if !result.verified
201            || result.agent_did.as_deref() != Some(claims.sub.as_str())
202            || result.op != Some(context.operation)
203            || result.service_did != context.service_did
204        {
205            return Err(ServiceError::Handler(
206                "AEP hosted verification did not recognize the client assertion".into(),
207            ));
208        }
209        Ok(claims)
210    }
211}