Skip to main content

aep_service/
authentication.rs

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}