1use aep_core::{
2 AssertionOperation, AuthenticationMethod, AuthorizationCarrier, BuiltInGrantResponse,
3 CredentialScheme, parse_built_in_grant_response, render_protected_resource_authorization,
4};
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use http::{HeaderMap, HeaderName, HeaderValue};
7
8use crate::{
9 AgentError, AuthenticationOptions, AuthenticationResult, CredentialRecord, Inspection, Session,
10 authorization, same_origin,
11};
12
13impl Session {
14 pub async fn authentication(
15 &self,
16 options: AuthenticationOptions,
17 ) -> Result<AuthenticationResult, AgentError> {
18 if options.client_assertion_only
19 && (options.credential_id.is_some() || options.grant_type.is_some())
20 {
21 return Err(AgentError::InvalidConfiguration("AEP credential selection cannot be combined with client-assertion-only authentication".to_owned()));
22 }
23 validate_resource(
24 &options.resource,
25 &self.service_url,
26 self.client.allow_insecure_loopback,
27 )?;
28 let inspection = self.inspect().await?;
29 let methods = inspection
30 .document
31 .authentication
32 .as_ref()
33 .map_or(&[][..], |authentication| authentication.methods.as_slice());
34 if let Some(grant_type) = options.grant_type.as_ref()
35 && !methods
36 .iter()
37 .any(|method| method.as_str() == grant_type.as_str())
38 {
39 return Err(AgentError::NoAuthenticationMethod);
40 }
41 if !options.client_assertion_only {
42 let credential_methods = implicit_credential_methods(methods, &options);
43 if !credential_methods.is_empty()
44 || options.credential_id.is_some()
45 || options.grant_type.is_some()
46 {
47 if let Some(record) = self
48 .find_credential(&inspection, credential_methods, &options)
49 .await?
50 {
51 return credential_authentication(
52 &record,
53 options.carrier,
54 self.client.clock.now(),
55 );
56 }
57 if options.credential_id.is_some() || options.grant_type.is_some() {
58 return Err(AgentError::Credential(
59 "requested AEP credential was not found".to_owned(),
60 ));
61 }
62 }
63 }
64 if !methods.contains(&AuthenticationMethod::AepJwt) {
65 return Err(AgentError::NoAuthenticationMethod);
66 }
67 let identity = self.resolve_identity(&inspection, true).await?;
68 let signer = self.client.identity_provider.signer_for(&identity).await?;
69 let assertion = self
70 .client
71 .sign_assertion(
72 &inspection,
73 &identity,
74 signer.as_ref(),
75 AssertionOperation::Authenticate,
76 Some(&options.resource),
77 )
78 .await?;
79 let (name, value) = render_protected_resource_authorization(&authorization(
80 options.carrier,
81 CredentialScheme::Aep,
82 assertion,
83 ))
84 .map_err(|error| AgentError::Credential(error.to_string()))?;
85 Ok(AuthenticationResult {
86 headers: one_header(&name, &value)?,
87 method: AuthenticationMethod::AepJwt,
88 })
89 }
90
91 pub async fn forget_credential(&self, credential_id: &str) -> Result<(), AgentError> {
92 if credential_id.is_empty() {
93 return Err(AgentError::Credential(
94 "AEP credential ID is required".to_owned(),
95 ));
96 }
97 let inspection = self.inspect().await?;
98 self.client
99 .credential_store
100 .delete(&inspection.document.service.did, credential_id)
101 .await
102 }
103
104 async fn find_credential(
105 &self,
106 inspection: &Inspection,
107 methods: &[AuthenticationMethod],
108 options: &AuthenticationOptions,
109 ) -> Result<Option<CredentialRecord>, AgentError> {
110 let service_did = &inspection.document.service.did;
111 if let Some(credential_id) = options.credential_id.as_deref() {
112 let Some(record) = self
113 .client
114 .credential_store
115 .find(service_did, credential_id)
116 .await?
117 else {
118 return Ok(None);
119 };
120 validate_record(&record, service_did, self.client.clock.now())?;
121 if options
122 .grant_type
123 .as_ref()
124 .is_some_and(|grant_type| grant_type != &record.grant_type)
125 {
126 return Err(AgentError::Credential(
127 "stored AEP credential does not match the requested grant type".to_owned(),
128 ));
129 }
130 if !methods
131 .iter()
132 .any(|method| method.as_str() == record.grant_type.as_str())
133 {
134 return Err(AgentError::NoAuthenticationMethod);
135 }
136 return Ok(Some(record));
137 }
138 let records = self.client.credential_store.list(service_did).await?;
139 for method in methods {
140 if let Some(record) = records.iter().find(|record| {
141 method.as_str() == record.grant_type.as_str()
142 && options
143 .grant_type
144 .as_ref()
145 .is_none_or(|grant_type| grant_type == &record.grant_type)
146 }) {
147 validate_record(record, service_did, self.client.clock.now())?;
148 return Ok(Some(record.clone()));
149 }
150 }
151 Ok(None)
152 }
153}
154
155fn implicit_credential_methods<'a>(
156 methods: &'a [AuthenticationMethod],
157 options: &AuthenticationOptions,
158) -> &'a [AuthenticationMethod] {
159 if options.credential_id.is_some() || options.grant_type.is_some() {
160 return methods;
161 }
162 methods
163 .iter()
164 .position(|method| method == &AuthenticationMethod::AepJwt)
165 .map_or(methods, |index| &methods[..index])
166}
167
168fn credential_authentication(
169 record: &CredentialRecord,
170 carrier: AuthorizationCarrier,
171 now: time::OffsetDateTime,
172) -> Result<AuthenticationResult, AgentError> {
173 validate_record(record, &record.service_did, now)?;
174 let encoded = serde_json::to_vec(&record.payload)?;
175 let credential = parse_built_in_grant_response(&record.grant_type, &encoded)?;
176 match credential {
177 BuiltInGrantResponse::OAuthBearer(value) => {
178 let (name, value) = render_protected_resource_authorization(&authorization(
179 carrier,
180 CredentialScheme::Bearer,
181 value.access_token,
182 ))
183 .map_err(|error| AgentError::Credential(error.to_string()))?;
184 Ok(AuthenticationResult {
185 headers: one_header(&name, &value)?,
186 method: AuthenticationMethod::OAuthBearer,
187 })
188 }
189 BuiltInGrantResponse::ApiKey(value) => Ok(AuthenticationResult {
190 headers: one_header(&value.header, &value.api_key)?,
191 method: AuthenticationMethod::ApiKey,
192 }),
193 BuiltInGrantResponse::Basic(value) => {
194 let credentials = STANDARD.encode(format!("{}:{}", value.username, value.password));
195 let (name, value) = render_protected_resource_authorization(&authorization(
196 carrier,
197 CredentialScheme::Basic,
198 credentials,
199 ))
200 .map_err(|error| AgentError::Credential(error.to_string()))?;
201 Ok(AuthenticationResult {
202 headers: one_header(&name, &value)?,
203 method: AuthenticationMethod::Basic,
204 })
205 }
206 }
207}
208
209pub(crate) fn validate_record(
210 record: &CredentialRecord,
211 service_did: &str,
212 now: time::OffsetDateTime,
213) -> Result<(), AgentError> {
214 if record.credential_id.is_empty()
215 || record.service_did != service_did
216 || record.expires_at <= now
217 {
218 return Err(AgentError::Credential(
219 "stored AEP credential metadata is invalid".to_owned(),
220 ));
221 }
222 let encoded = serde_json::to_vec(&record.payload)?;
223 let credential = parse_built_in_grant_response(&record.grant_type, &encoded)?;
224 let (credential_id, expires_at) = match credential {
225 BuiltInGrantResponse::OAuthBearer(value) => (value.credential_id, value.expires_at),
226 BuiltInGrantResponse::ApiKey(value) => (value.credential_id, value.expires_at),
227 BuiltInGrantResponse::Basic(value) => (value.credential_id, value.expires_at),
228 };
229 let expires_at =
230 time::OffsetDateTime::parse(&expires_at, &time::format_description::well_known::Rfc3339)
231 .map_err(|_| {
232 AgentError::Credential("stored AEP credential expiration is invalid".to_owned())
233 })?;
234 if credential_id != record.credential_id || expires_at != record.expires_at {
235 return Err(AgentError::Credential(
236 "stored AEP credential metadata does not match its payload".to_owned(),
237 ));
238 }
239 Ok(())
240}
241
242fn validate_resource(
243 resource: &url::Url,
244 service: &url::Url,
245 allow_insecure_loopback: bool,
246) -> Result<(), AgentError> {
247 if !resource.username().is_empty()
248 || resource.password().is_some()
249 || resource.fragment().is_some()
250 || resource.host_str().is_none()
251 {
252 return Err(AgentError::InvalidServiceReference(
253 "AEP protected resource URL is invalid".to_owned(),
254 ));
255 }
256 if resource.scheme() != "https"
257 && !(allow_insecure_loopback && resource.scheme() == "http" && crate::is_loopback(resource))
258 {
259 return Err(AgentError::InvalidServiceReference(
260 "AEP protected resource requires HTTPS".to_owned(),
261 ));
262 }
263 if !same_origin(resource, service) {
264 return Err(AgentError::InvalidServiceReference(
265 "AEP protected resource must use the Service origin".to_owned(),
266 ));
267 }
268 Ok(())
269}
270
271fn one_header(name: &str, value: &str) -> Result<HeaderMap, AgentError> {
272 let name = HeaderName::from_bytes(name.as_bytes())
273 .map_err(|_| AgentError::Credential("AEP credential header name is invalid".to_owned()))?;
274 let value = HeaderValue::from_str(value)
275 .map_err(|_| AgentError::Credential("AEP credential header value is invalid".to_owned()))?;
276 let mut headers = HeaderMap::new();
277 headers.insert(name, value);
278 Ok(headers)
279}