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 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}