1use std::{sync::Arc, time::Duration};
2
3use aep_core::{
4 AUTHORIZATION_HEADER, AssertionOperation, AuthenticationMethod, AuthorizationCarrier,
5 ClientAssertionClaims, CredentialScheme, IdentityMethod, ResolveDidWebPublicKeyOptions,
6 VerifyClientAssertionOptions, decode_jwt_unverified, parse_protected_resource_authorization,
7 resolve_did_web_public_key, validate_client_assertion_claims_with_options,
8 verify_client_assertion,
9};
10use async_trait::async_trait;
11use http::{HeaderMap, header};
12use time::{Duration as TimeDuration, OffsetDateTime};
13use url::Url;
14
15use crate::{
16 ClientAssertionReplayRecord, ClientAssertionReplayStore, ClientAssertionVerificationContext,
17 ClientAssertionVerifier, Clock, ServiceError,
18};
19
20pub(crate) struct AssertionAuthentication<'a> {
21 pub allow_insecure_loopback: bool,
22 pub assertion: &'a str,
23 pub clock: &'a dyn Clock,
24 pub idempotency_key: Option<&'a str>,
25 pub identity_methods: &'a [IdentityMethod],
26 pub maximum_clock_skew: Duration,
27 pub operation: AssertionOperation,
28 pub replay_store: &'a Arc<dyn ClientAssertionReplayStore>,
29 pub resource: Option<&'a Url>,
30 pub service_did: &'a str,
31 pub signing_algorithms: &'a [aep_core::SigningAlgorithm],
32 pub verifier: &'a Arc<dyn ClientAssertionVerifier>,
33}
34
35pub(crate) async fn authenticate_assertion(
36 options: AssertionAuthentication<'_>,
37) -> Result<Option<ClientAssertionClaims>, ServiceError> {
38 let now = options.clock.now();
39 let context = ClientAssertionVerificationContext {
40 assertion: options.assertion.to_owned(),
41 current_time: now,
42 idempotency_key: options.idempotency_key.map(str::to_owned),
43 operation: options.operation,
44 resource: options.resource.cloned(),
45 service_did: options.service_did.to_owned(),
46 signing_algorithms: options.signing_algorithms.to_vec(),
47 };
48 let Ok(claims) = options.verifier.verify(context).await else {
49 return Ok(None);
50 };
51 if validate_client_assertion_claims_with_options(
52 &claims,
53 aep_core::ClientAssertionValidationOptions {
54 allow_insecure_loopback: options.allow_insecure_loopback,
55 },
56 )
57 .is_err()
58 || claims.aud != options.service_did
59 || claims.iss != claims.sub
60 || claims.op != options.operation
61 || claims.resource.as_deref() != options.resource.map(Url::as_str)
62 || !supports_identity_method(&claims.sub, options.identity_methods)
63 || !inside_validity_window(&claims, now, options.maximum_clock_skew)
64 {
65 return Ok(None);
66 }
67 let skew = TimeDuration::try_from(options.maximum_clock_skew)
68 .map_err(|_| ServiceError::InvalidConfiguration("clock skew is too large".to_owned()))?;
69 let Some(expiration) = OffsetDateTime::from_unix_timestamp(claims.exp)
70 .ok()
71 .and_then(|expiration| expiration.checked_add(skew))
72 else {
73 return Ok(None);
74 };
75 let consumed = options
76 .replay_store
77 .consume(
78 ClientAssertionReplayRecord {
79 expires_at: expiration,
80 jti: claims.jti.clone(),
81 sub: claims.sub.clone(),
82 },
83 now,
84 )
85 .await?;
86 Ok(consumed.then_some(claims))
87}
88
89#[async_trait]
90impl crate::ClientAssertionVerifier for crate::DidWebClientAssertionVerifier {
91 async fn verify(
92 &self,
93 context: ClientAssertionVerificationContext,
94 ) -> Result<ClientAssertionClaims, ServiceError> {
95 let decoded = decode_jwt_unverified(&context.assertion)?;
96 let issuer = decoded
97 .payload
98 .get("iss")
99 .and_then(|value| value.as_str())
100 .ok_or_else(|| ServiceError::Handler("client assertion iss is required".to_owned()))?;
101 let key_id =
102 decoded.header.key_id.as_deref().ok_or_else(|| {
103 ServiceError::Handler("client assertion kid is required".to_owned())
104 })?;
105 let key = resolve_did_web_public_key(ResolveDidWebPublicKeyOptions {
106 algorithm: decoded.header.algorithm,
107 allow_insecure_loopback: self.allow_insecure_loopback,
108 did: issuer,
109 key_id,
110 transport: self.transport.as_ref(),
111 })
112 .await?;
113 verify_client_assertion(
114 &context.assertion,
115 &key,
116 &VerifyClientAssertionOptions {
117 algorithms: context.signing_algorithms,
118 allow_insecure_loopback: self.allow_insecure_loopback,
119 audience: Some(context.service_did),
120 current_time: Some(context.current_time.unix_timestamp()),
121 issuer: Some(issuer.to_owned()),
122 operation: Some(context.operation),
123 resource: context.resource.map(|resource| resource.to_string()),
124 subject: Some(issuer.to_owned()),
125 ..VerifyClientAssertionOptions::default()
126 },
127 )
128 .map_err(ServiceError::from)
129 }
130}
131
132pub(crate) enum SelectedAuthorization {
133 Aep(String),
134 Session(AuthenticationMethod),
135 Unrelated,
136 Unauthenticated,
137}
138
139pub(crate) fn select_authorization(headers: &HeaderMap) -> Result<SelectedAuthorization, ()> {
140 let dedicated_values = headers
141 .get_all(AUTHORIZATION_HEADER)
142 .iter()
143 .collect::<Vec<_>>();
144 if dedicated_values.len() > 1 {
145 return Err(());
146 }
147 let standard_values = headers
148 .get_all(header::AUTHORIZATION)
149 .iter()
150 .collect::<Vec<_>>();
151 if standard_values.len() > 1 {
152 return Err(());
153 }
154 if let Some(value) = dedicated_values.first() {
155 let value = value.to_str().map_err(|_| ())?;
156 let parsed = parse_protected_resource_authorization(value, AuthorizationCarrier::Dedicated)
157 .map_err(|_| ())?;
158 if standard_values.first().is_some_and(|standard| {
159 standard.to_str().is_ok_and(|standard| {
160 parse_protected_resource_authorization(standard, AuthorizationCarrier::Standard)
161 .is_ok()
162 })
163 }) {
164 return Err(());
165 }
166 return match parsed.scheme {
167 CredentialScheme::Aep => Ok(SelectedAuthorization::Aep(parsed.credentials)),
168 CredentialScheme::Bearer => Ok(SelectedAuthorization::Session(
169 AuthenticationMethod::OAuthBearer,
170 )),
171 CredentialScheme::Basic => {
172 Ok(SelectedAuthorization::Session(AuthenticationMethod::Basic))
173 }
174 };
175 }
176 let Some(value) = standard_values.first() else {
177 return Ok(SelectedAuthorization::Unauthenticated);
178 };
179 let recognized = value
180 .as_bytes()
181 .split(|byte| matches!(byte, b' ' | b'\t'))
182 .next()
183 .is_some_and(|scheme| {
184 [b"aep".as_slice(), b"bearer".as_slice(), b"basic".as_slice()]
185 .iter()
186 .any(|known| scheme.eq_ignore_ascii_case(known))
187 });
188 let Ok(value) = value.to_str() else {
189 return if recognized {
190 Err(())
191 } else {
192 Ok(SelectedAuthorization::Unrelated)
193 };
194 };
195 match parse_protected_resource_authorization(value, AuthorizationCarrier::Standard) {
196 Ok(parsed) if parsed.scheme == CredentialScheme::Aep => {
197 Ok(SelectedAuthorization::Aep(parsed.credentials))
198 }
199 Ok(parsed) if parsed.scheme == CredentialScheme::Bearer => Ok(
200 SelectedAuthorization::Session(AuthenticationMethod::OAuthBearer),
201 ),
202 Ok(parsed) if parsed.scheme == CredentialScheme::Basic => {
203 Ok(SelectedAuthorization::Session(AuthenticationMethod::Basic))
204 }
205 Err(_) if recognized => Err(()),
206 Ok(_) | Err(_) => Ok(SelectedAuthorization::Unrelated),
207 }
208}
209
210fn inside_validity_window(
211 claims: &ClientAssertionClaims,
212 now: OffsetDateTime,
213 maximum_clock_skew: Duration,
214) -> bool {
215 let Ok(skew) = i64::try_from(maximum_clock_skew.as_secs()) else {
216 return false;
217 };
218 let timestamp = now.unix_timestamp();
219 claims.iat <= timestamp.saturating_add(skew) && claims.exp > timestamp.saturating_sub(skew)
220}
221
222fn supports_identity_method(agent_did: &str, methods: &[IdentityMethod]) -> bool {
223 methods.iter().any(|method| match method {
224 IdentityMethod::DidWeb => agent_did.starts_with("did:web:"),
225 IdentityMethod::Other(method) => agent_did.starts_with(&format!("{method}:")),
226 })
227}