1use std::{collections::BTreeMap, fmt, sync::Arc, time::Duration};
2
3use aep_core::{
4 AgentStatus, ApiKeyGrantResponse, AssertionOperation, AuthenticationMethod, BasicGrantResponse,
5 BuiltInGrantResponse, ClaimName, ClaimValues, ClientAssertionClaims, EnrollRequest,
6 EnrollmentDecisionStatus, ErrorCode, GrantRequest, GrantType, GrantTypeConfig, IdentityMethod,
7 InspectDocument, OAuthBearerGrantResponse, ProblemDetails, RevokeRequest, SigningAlgorithm,
8};
9use async_trait::async_trait;
10use http::HeaderMap;
11use serde_json::Value;
12use time::OffsetDateTime;
13use url::Url;
14
15use crate::ServiceError;
16
17#[derive(Clone, PartialEq)]
18pub enum ResponseBody {
19 Enroll(aep_core::EnrollResponse),
20 Grant(Value),
21 Problem(ProblemDetails),
22 Revoke(aep_core::RevokeResponse),
23 Status(aep_core::StatusResponse),
24}
25
26impl fmt::Debug for ResponseBody {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Enroll(value) => formatter.debug_tuple("Enroll").field(value).finish(),
30 Self::Grant(_) => formatter.debug_tuple("Grant").field(&"[REDACTED]").finish(),
31 Self::Problem(value) => formatter.debug_tuple("Problem").field(value).finish(),
32 Self::Revoke(value) => formatter.debug_tuple("Revoke").field(value).finish(),
33 Self::Status(value) => formatter.debug_tuple("Status").field(value).finish(),
34 }
35 }
36}
37
38impl ResponseBody {
39 pub const fn content_type(&self) -> &'static str {
40 match self {
41 Self::Problem(_) => aep_core::PROBLEM_MEDIA_TYPE,
42 Self::Enroll(_) | Self::Grant(_) | Self::Revoke(_) | Self::Status(_) => {
43 aep_core::MEDIA_TYPE
44 }
45 }
46 }
47
48 pub fn to_json(&self) -> Result<Value, serde_json::Error> {
49 match self {
50 Self::Enroll(value) => serde_json::to_value(value),
51 Self::Grant(value) => Ok(value.clone()),
52 Self::Problem(value) => serde_json::to_value(value),
53 Self::Revoke(value) => serde_json::to_value(value),
54 Self::Status(value) => serde_json::to_value(value),
55 }
56 }
57}
58
59#[derive(Clone, PartialEq)]
60pub struct ServiceResponse {
61 pub body: ResponseBody,
62 pub headers: HeaderMap,
63 pub status: u16,
64}
65
66impl fmt::Debug for ServiceResponse {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 formatter
69 .debug_struct("ServiceResponse")
70 .field("body", &self.body)
71 .field("headers", &"[REDACTED]")
72 .field("status", &self.status)
73 .finish()
74 }
75}
76
77impl ServiceResponse {
78 pub fn to_json(&self) -> Result<Value, serde_json::Error> {
79 self.body.to_json()
80 }
81}
82
83#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
84pub enum IdempotentCommand {
85 Enroll,
86 Grant,
87 Revoke,
88}
89
90#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct CommandIdempotencyInput {
92 pub agent_did: String,
93 pub command: IdempotentCommand,
94 pub idempotency_key: String,
95 pub request_hash: String,
96}
97
98#[derive(Clone, Debug, PartialEq)]
99pub struct CommandIdempotencyRecord {
100 pub input: CommandIdempotencyInput,
101 pub response: ServiceResponse,
102 pub created_at: OffsetDateTime,
103}
104
105#[derive(Clone, Debug, PartialEq)]
106pub enum CommandIdempotencyResult {
107 Created(ServiceResponse),
108 Replayed(ServiceResponse),
109 Conflict,
110}
111
112pub type CommandOperation = Box<
113 dyn FnOnce() -> futures::future::BoxFuture<'static, Result<ServiceResponse, ServiceError>>
114 + Send,
115>;
116
117#[async_trait]
118pub trait CommandIdempotencyStore: Send + Sync {
119 async fn execute(
120 &self,
121 input: CommandIdempotencyInput,
122 operation: CommandOperation,
123 ) -> Result<CommandIdempotencyResult, ServiceError>;
124}
125
126#[derive(Clone, Debug, PartialEq)]
127pub struct EnrollmentRecord {
128 pub agent_did: String,
129 pub claims: ClaimValues,
130 pub created_at: OffsetDateTime,
131 pub owner_action_required: bool,
132 pub requirements_pending: Vec<String>,
133 pub since: OffsetDateTime,
134 pub status: AgentStatus,
135 pub updated_at: OffsetDateTime,
136 pub verification_pending: Vec<String>,
137}
138
139#[async_trait]
140pub trait EnrollmentStore: Send + Sync {
141 async fn find(&self, agent_did: &str) -> Result<Option<EnrollmentRecord>, ServiceError>;
142 async fn save(&self, record: EnrollmentRecord) -> Result<EnrollmentRecord, ServiceError>;
143}
144
145#[derive(Clone, Debug, PartialEq)]
146pub struct EnrollmentDecision {
147 pub owner_action_required: bool,
148 pub requirements_pending: Vec<String>,
149 pub status: EnrollmentDecisionStatus,
150 pub verification_pending: Vec<String>,
151}
152
153impl Default for EnrollmentDecision {
154 fn default() -> Self {
155 Self {
156 owner_action_required: false,
157 requirements_pending: Vec::new(),
158 status: EnrollmentDecisionStatus::Active,
159 verification_pending: Vec::new(),
160 }
161 }
162}
163
164#[async_trait]
165pub trait EnrollmentPolicy: Send + Sync {
166 async fn decide(
167 &self,
168 request: &EnrollRequest,
169 now: OffsetDateTime,
170 ) -> Result<EnrollmentDecision, ServiceError>;
171}
172
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct ClientAssertionReplayRecord {
175 pub expires_at: OffsetDateTime,
176 pub jti: String,
177 pub sub: String,
178}
179
180#[async_trait]
181pub trait ClientAssertionReplayStore: Send + Sync {
182 async fn consume(
183 &self,
184 record: ClientAssertionReplayRecord,
185 now: OffsetDateTime,
186 ) -> Result<bool, ServiceError>;
187}
188
189#[derive(Clone, PartialEq)]
190pub struct ClientAssertionVerificationContext {
191 pub assertion: String,
192 pub current_time: OffsetDateTime,
193 pub idempotency_key: Option<String>,
194 pub operation: AssertionOperation,
195 pub resource: Option<Url>,
196 pub service_did: String,
197 pub signing_algorithms: Vec<SigningAlgorithm>,
198}
199
200impl fmt::Debug for ClientAssertionVerificationContext {
201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202 formatter
203 .debug_struct("ClientAssertionVerificationContext")
204 .field("assertion", &"[REDACTED]")
205 .field("current_time", &self.current_time)
206 .field("idempotency_key", &self.idempotency_key)
207 .field("operation", &self.operation)
208 .field("resource", &self.resource.as_ref().map(|_| "[REDACTED]"))
209 .field("service_did", &self.service_did)
210 .field("signing_algorithms", &self.signing_algorithms)
211 .finish()
212 }
213}
214
215#[async_trait]
216pub trait ClientAssertionVerifier: Send + Sync {
217 async fn verify(
218 &self,
219 context: ClientAssertionVerificationContext,
220 ) -> Result<ClientAssertionClaims, ServiceError>;
221}
222
223pub struct DidWebClientAssertionVerifier {
224 pub(crate) allow_insecure_loopback: bool,
225 pub(crate) transport: Arc<dyn aep_core::HttpTransport>,
226}
227
228impl DidWebClientAssertionVerifier {
229 pub fn new(transport: Arc<dyn aep_core::HttpTransport>, allow_insecure_loopback: bool) -> Self {
230 Self {
231 allow_insecure_loopback,
232 transport,
233 }
234 }
235}
236
237#[derive(Clone, Debug, PartialEq)]
238pub struct GrantContext {
239 pub agent_did: String,
240 pub enrollment: EnrollmentRecord,
241 pub grant_type: GrantType,
242 pub now: OffsetDateTime,
243}
244
245#[derive(Clone)]
246pub struct CredentialAuthenticationInput {
247 pub headers: HeaderMap,
248 pub now: OffsetDateTime,
249}
250
251impl fmt::Debug for CredentialAuthenticationInput {
252 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
253 formatter
254 .debug_struct("CredentialAuthenticationInput")
255 .field("headers", &"[REDACTED]")
256 .field("now", &self.now)
257 .finish()
258 }
259}
260
261#[derive(Clone, Debug, Eq, PartialEq)]
262pub enum AuthenticationKind {
263 ClientAssertion,
264 SessionCredential,
265}
266
267#[derive(Clone, Debug, PartialEq)]
268pub struct AuthenticatedPrincipal {
269 pub agent_did: String,
270 pub authentication_kind: AuthenticationKind,
271 pub authentication_method: AuthenticationMethod,
272 pub credential_id: Option<String>,
273 pub grant_type: Option<GrantType>,
274 pub scopes: Vec<String>,
275}
276
277#[async_trait]
278pub trait GrantTypeHandler: Send + Sync {
279 async fn grant(
280 &self,
281 request: &GrantRequest,
282 context: &GrantContext,
283 ) -> Result<Value, ServiceError>;
284 async fn revoke(
285 &self,
286 request: &RevokeRequest,
287 context: &GrantContext,
288 ) -> Result<(), ServiceError>;
289 async fn authenticate(
290 &self,
291 _input: &CredentialAuthenticationInput,
292 ) -> Result<Option<AuthenticatedPrincipal>, ServiceError> {
293 Ok(None)
294 }
295 async fn has_presentation(
296 &self,
297 _input: &CredentialAuthenticationInput,
298 ) -> Result<bool, ServiceError> {
299 Ok(false)
300 }
301}
302
303#[derive(Clone, Debug, PartialEq)]
304pub struct ServiceCredentialRecord {
305 pub agent_did: String,
306 pub created_at: OffsetDateTime,
307 pub credential: BuiltInGrantResponse,
308 pub credential_id: String,
309 pub expires_at: OffsetDateTime,
310 pub grant_type: GrantType,
311}
312
313#[derive(Clone, Debug, PartialEq)]
314pub struct CredentialMatch {
315 pub agent_did: String,
316 pub credential_id: String,
317 pub expires_at: OffsetDateTime,
318 pub grant_type: GrantType,
319 pub scopes: Vec<String>,
320}
321
322#[async_trait]
323pub trait ServiceCredentialStore: Send + Sync {
324 async fn authenticate(
325 &self,
326 grant_type: &GrantType,
327 input: &CredentialAuthenticationInput,
328 ) -> Result<Option<CredentialMatch>, ServiceError>;
329 async fn has_presentation(
330 &self,
331 grant_type: &GrantType,
332 input: &CredentialAuthenticationInput,
333 ) -> Result<bool, ServiceError>;
334 async fn revoke_credential(
335 &self,
336 agent_did: &str,
337 grant_type: &GrantType,
338 credential_id: &str,
339 revoked_at: OffsetDateTime,
340 ) -> Result<(), ServiceError>;
341 async fn revoke_grant_type(
342 &self,
343 agent_did: &str,
344 grant_type: &GrantType,
345 revoked_at: OffsetDateTime,
346 ) -> Result<(), ServiceError>;
347 async fn save(&self, record: ServiceCredentialRecord) -> Result<(), ServiceError>;
348}
349
350pub type BuiltInCredentialIssuer<T> = Arc<
351 dyn Fn(
352 GrantRequest,
353 GrantContext,
354 ) -> futures::future::BoxFuture<'static, Result<T, ServiceError>>
355 + Send
356 + Sync,
357>;
358
359pub struct StoredCredentialGrantTypeOptions<T> {
360 pub config: GrantTypeConfig,
361 pub issue: BuiltInCredentialIssuer<T>,
362 pub store: Arc<dyn ServiceCredentialStore>,
363}
364
365pub type StoredOAuthBearerGrantTypeOptions =
366 StoredCredentialGrantTypeOptions<OAuthBearerGrantResponse>;
367pub type StoredApiKeyGrantTypeOptions = StoredCredentialGrantTypeOptions<ApiKeyGrantResponse>;
368pub type StoredBasicGrantTypeOptions = StoredCredentialGrantTypeOptions<BasicGrantResponse>;
369
370#[derive(Clone)]
371pub struct GrantTypeDefinition {
372 pub config: Option<GrantTypeConfig>,
373 pub grant_type: GrantType,
374 pub handler: Option<Arc<dyn GrantTypeHandler>>,
375}
376
377#[derive(Clone, Debug, Default, PartialEq)]
378pub struct ClaimsConfiguration {
379 pub optional: Vec<ClaimName>,
380 pub preferred: Vec<ClaimName>,
381 pub required: Vec<ClaimName>,
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
385pub struct OpenApiConfiguration {
386 pub trailing_slash: aep_core::OpenApiTrailingSlash,
387 pub url: String,
388}
389
390pub trait Clock: Send + Sync {
391 fn now(&self) -> OffsetDateTime;
392}
393
394#[derive(Clone, Copy, Debug, Eq, PartialEq)]
400pub struct ClaimValueLimits {
401 pub maximum_encoded_bytes: usize,
402 pub maximum_member_count: usize,
403 pub maximum_object_depth: usize,
404 pub maximum_string_length: usize,
405}
406
407impl Default for ClaimValueLimits {
408 fn default() -> Self {
409 Self {
410 maximum_encoded_bytes: 65_536,
411 maximum_member_count: 128,
412 maximum_object_depth: 8,
413 maximum_string_length: 4_096,
414 }
415 }
416}
417
418impl ClaimValueLimits {
419 pub(crate) fn validate(self) -> Result<Self, ServiceError> {
420 if self.maximum_encoded_bytes == 0
421 || self.maximum_member_count == 0
422 || self.maximum_object_depth == 0
423 || self.maximum_string_length == 0
424 {
425 return Err(ServiceError::InvalidConfiguration(
426 "claim value limits must be positive".to_owned(),
427 ));
428 }
429 Ok(self)
430 }
431}
432
433#[derive(Clone)]
434pub struct ServiceOptions {
435 pub allow_insecure_loopback: bool,
436 pub authentication_methods: Vec<AuthenticationMethod>,
437 pub clock: Option<Arc<dyn Clock>>,
438 pub claims: ClaimsConfiguration,
439 pub claim_value_limits: ClaimValueLimits,
440 pub endpoint_base: Option<String>,
441 pub enrollment_policy: Option<Arc<dyn EnrollmentPolicy>>,
442 pub enrollment_store: Option<Arc<dyn EnrollmentStore>>,
443 pub extensions: Vec<String>,
444 pub grant_types: Vec<GrantTypeDefinition>,
445 pub identity_methods: Vec<IdentityMethod>,
446 pub idempotency_store: Option<Arc<dyn CommandIdempotencyStore>>,
447 pub inspect_url: Option<Url>,
448 pub maximum_clock_skew: Duration,
449 pub openapi: Option<OpenApiConfiguration>,
450 pub replay_store: Option<Arc<dyn ClientAssertionReplayStore>>,
451 pub service_did: String,
452 pub signing_algorithms: Vec<SigningAlgorithm>,
453 pub verifier: Arc<dyn ClientAssertionVerifier>,
454}
455
456impl ServiceOptions {
457 pub fn new(service_did: impl Into<String>, verifier: Arc<dyn ClientAssertionVerifier>) -> Self {
458 Self {
459 allow_insecure_loopback: false,
460 authentication_methods: Vec::new(),
461 clock: None,
462 claims: ClaimsConfiguration::default(),
463 claim_value_limits: ClaimValueLimits::default(),
464 endpoint_base: None,
465 enrollment_policy: None,
466 enrollment_store: None,
467 extensions: Vec::new(),
468 grant_types: Vec::new(),
469 identity_methods: vec![IdentityMethod::DidWeb],
470 idempotency_store: None,
471 inspect_url: None,
472 maximum_clock_skew: aep_core::RECOMMENDED_CLOCK_SKEW,
473 openapi: None,
474 replay_store: None,
475 service_did: service_did.into(),
476 signing_algorithms: vec![SigningAlgorithm::EdDsa, SigningAlgorithm::Es256],
477 verifier,
478 }
479 }
480}
481
482#[derive(Clone)]
483pub struct AuthenticatedCommandOptions {
484 pub client_assertion: String,
485}
486
487impl fmt::Debug for AuthenticatedCommandOptions {
488 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
489 formatter
490 .debug_struct("AuthenticatedCommandOptions")
491 .field("client_assertion", &"[REDACTED]")
492 .finish()
493 }
494}
495
496#[derive(Clone)]
497pub struct IdempotentCommandOptions {
498 pub client_assertion: String,
499 pub idempotency_key: String,
500}
501
502impl fmt::Debug for IdempotentCommandOptions {
503 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
504 formatter
505 .debug_struct("IdempotentCommandOptions")
506 .field("client_assertion", &"[REDACTED]")
507 .field("idempotency_key", &self.idempotency_key)
508 .finish()
509 }
510}
511
512#[derive(Clone)]
513pub struct ProtectedResourceRequest {
514 pub headers: HeaderMap,
515 pub method: http::Method,
516 pub url: Url,
517}
518
519impl fmt::Debug for ProtectedResourceRequest {
520 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
521 formatter
522 .debug_struct("ProtectedResourceRequest")
523 .field("headers", &"[REDACTED]")
524 .field("method", &self.method)
525 .field("url", &"[REDACTED]")
526 .finish()
527 }
528}
529
530#[derive(Clone, Debug, PartialEq)]
531pub enum ProtectedResourceAuthentication {
532 Authenticated(AuthenticatedPrincipal),
533 Rejected(ServiceResponse),
534}
535
536#[derive(Clone)]
537pub(crate) struct ServiceConfiguration {
538 pub authentication_methods: Vec<AuthenticationMethod>,
539 pub claim_value_limits: ClaimValueLimits,
540 pub claims: ClaimsConfiguration,
541 pub grant_handlers: BTreeMap<String, Arc<dyn GrantTypeHandler>>,
542 pub inspect_document: InspectDocument,
543 pub inspect_url: Option<Url>,
544 pub maximum_clock_skew: Duration,
545 pub service_did: String,
546 pub signing_algorithms: Vec<SigningAlgorithm>,
547}
548
549pub(crate) fn problem(code: ErrorCode, status: u16) -> ServiceResponse {
550 let mut characters = code.as_str().replace('_', " ").into_bytes();
553 if let Some(first) = characters.first_mut() {
554 first.make_ascii_uppercase();
555 }
556 let title = String::from_utf8(characters).unwrap_or_else(|_| code.as_str().to_owned());
557 let body = ResponseBody::Problem(aep_core::new_problem_details(
558 code,
559 title,
560 i64::from(status),
561 ));
562 let mut headers = HeaderMap::new();
563 headers.insert(
564 http::header::CONTENT_TYPE,
565 http::HeaderValue::from_static(body.content_type()),
566 );
567 ServiceResponse {
568 body,
569 headers,
570 status,
571 }
572}