Skip to main content

aep_service/
types.rs

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/// The bounds a Service places on the Claim Values it will accept.
395///
396/// CLM-SEC-006 requires a Service to bound the encoded size, string length, object depth, and
397/// member count of the claim document under local policy. These are the defaults; a Service that
398/// needs different ones sets them on [`ServiceOptions`].
399#[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    // The registry spells each code in lower snake case; the published titles are the same words
551    // as one sentence, so only the first is capitalised.
552    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}