pubhubs/servers/auths/
auth.rs1use super::server::*;
3
4use std::collections::HashMap;
5use std::rc::Rc;
6
7use actix_web::web;
8use indexmap::IndexMap;
9
10use crate::servers::{self, yivi};
11use crate::{
12 api::{self, ResultExt as _},
13 attr, handle,
14 misc::jwt,
15};
16
17impl App {
19 pub async fn handle_auth_start(
21 app: Rc<Self>,
22 req: web::Json<api::auths::AuthStartReq>,
23 ) -> api::Result<api::auths::AuthStartResp> {
24 let req = req.into_inner();
25
26 if req.yivi_chained_session && req.source != attr::Source::Yivi {
27 log::debug!("yivi_chained_session set on non-yivi authentication request");
28 return Err(api::ErrorCode::BadRequest);
29 }
30
31 if req.yivi_chained_session_drip && !req.yivi_chained_session {
32 log::debug!(
33 "yivi_chained_session_drip set on authentication request while yivi_chained_session is not"
34 );
35 return Err(api::ErrorCode::BadRequest);
36 }
37
38 if !req.attr_type_choices.is_empty() && !req.attr_types.is_empty() {
39 log::debug!("both attr_types and attr_type_choices set on AuthStartReq");
40 return Err(api::ErrorCode::BadRequest);
41 }
42
43 let max = app.max_attr_types_per_req;
46 if req.attr_types.len() > max
47 || req.attr_type_choices.len() > max
48 || req
49 .attr_type_choices
50 .iter()
51 .any(|choices| choices.len() > max)
52 {
53 log::debug!(
54 "rejecting AuthStartReq: it requests more than {max} attribute types (or alternatives \
55 per type); this limit is set by the authentication server's `max_attr_types_per_req` \
56 config option"
57 );
58 return Err(api::ErrorCode::BadRequest);
59 }
60
61 let attr_type_choices = if req.attr_type_choices.is_empty() {
62 req.attr_types.into_iter().map(|at| vec![at]).collect()
63 } else {
64 req.attr_type_choices
65 };
66
67 let state = AuthState {
68 source: req.source,
69 attr_type_choices,
70 exp: api::NumericDate::now().add_clamp(app.auth_window.as_secs()),
71 yivi_chained_session: None,
72 yivi_ati2at: Default::default(),
73 };
74
75 match req.source {
76 attr::Source::Yivi => {
77 Self::handle_auth_start_yivi(
78 app,
79 state,
80 req.yivi_chained_session,
81 req.yivi_chained_session_drip,
82 )
83 .await
84 }
85 }
86 }
87
88 fn create_disclosure_con_for(
100 &self,
101 attr_type_id: &servers::yivi::AttributeTypeIdentifier,
102 ) -> api::Result<Vec<servers::yivi::AttributeRequest>> {
103 let yivi = self.get_yivi()?;
104
105 let mut result = vec![servers::yivi::AttributeRequest {
106 ty: attr_type_id.clone(),
107 value: None,
108 }];
109
110 let credential = yivi.card_config.card_type.credential();
111
112 if !attr_type_id.as_str().starts_with(credential) {
113 return Ok(result);
114 }
115
116 let registration_date = yivi.card_config.card_type.date();
117
118 result.push(servers::yivi::AttributeRequest {
119 ty: format!("{credential}.{registration_date}")
120 .parse()
121 .map_err(|err| {
122 log::error!("failed to form registration date yivi attribute: {err:?}");
123 api::ErrorCode::InternalError
124 })?,
125 value: None,
126 });
127
128 let registration_source = yivi.card_config.card_type.source();
129
130 result.push(servers::yivi::AttributeRequest {
131 ty: format!("{credential}.{registration_source}")
132 .parse()
133 .map_err(|err| {
134 log::error!("failed to form registration source yivi attribute: {err:?}");
135 api::ErrorCode::InternalError
136 })?,
137 value: Some(self.registration_source(yivi).to_owned()),
138 });
139
140 Ok(result)
141 }
142
143 async fn handle_auth_start_yivi(
144 app: Rc<Self>,
145 mut state: AuthState,
146 yivi_chained_session: bool,
147 yivi_chained_session_drip: bool,
148 ) -> api::Result<api::auths::AuthStartResp> {
149 let yivi = app.get_yivi()?;
150
151 let mut sealed_state: Option<api::auths::AuthState> = None;
152
153 let seal_state = |state: &AuthState| -> api::Result<api::auths::AuthState> {
154 state.seal(&app.auth_state_secret)
155 };
156
157 let mut cdc: servers::yivi::AttributeConDisCon = Default::default(); for attr_ty_options in state.attr_type_choices.iter() {
161 let mut dc: Vec<Vec<servers::yivi::AttributeRequest>> = Default::default();
162 let mut ati2at: HashMap<yivi::AttributeTypeIdentifier, handle::Handle> =
163 Default::default();
164
165 for attr_ty_handle in attr_ty_options.iter() {
166 let Some(attr_ty) = app.attr_type_from_handle(attr_ty_handle) else {
167 return Ok(api::auths::AuthStartResp::UnknownAttrType(
168 attr_ty_handle.clone(),
169 ));
170 };
171
172 let mut had_one: bool = false;
173
174 for ati in attr_ty.yivi_attr_type_ids() {
175 if let Some(existing_at_handle) =
176 ati2at.insert(ati.clone(), attr_ty_handle.clone())
177 {
178 log::debug!(
179 "attribute types {existing_at_handle} and {attr_ty_handle} both rely on the same yivi attribute type identifier {ati}"
180 );
181 return Ok(api::auths::AuthStartResp::Conflict(
182 existing_at_handle,
183 attr_ty_handle.clone(),
184 ));
185 }
186
187 had_one = true;
188
189 dc.push(app.create_disclosure_con_for(ati)?);
190 }
191
192 if !had_one {
193 log::debug!(
194 "got yivi authentication start request for {attr_ty_handle}, but yivi is not supported for this attribute type",
195 );
196 return Ok(api::auths::AuthStartResp::SourceNotAvailableFor(
197 attr_ty_handle.clone(),
198 ));
199 }
200 }
201
202 state.yivi_ati2at.push(ati2at);
203 cdc.push(dc);
204 }
205
206 let disclosure_request: jwt::JWT = {
207 let mut dr = servers::yivi::ExtendedSessionRequest::disclosure(cdc);
208
209 if yivi_chained_session {
210 let csc = app.chained_sessions_ctl_or_bad_request()?;
211 let running_state = app.running_state_or_internal_error()?;
212
213 let Some(chained_session_id) = csc.create_session().await? else {
214 log::debug!(
215 "refusing yivi chained session: authentication server at its chained-session capacity"
216 );
217 return Ok(api::auths::AuthStartResp::ChainedSessionsTemporarilyUnavailable);
218 };
219
220 state.yivi_chained_session = Some(ChainedSessionSetup {
221 id: chained_session_id,
222 drip: yivi_chained_session_drip,
223 });
224
225 sealed_state = Some(seal_state(&state)?);
226
227 let query = serde_urlencoded::to_string(api::auths::YiviNextSessionQuery {
228 state: sealed_state.as_ref().unwrap().clone(),
229 })
230 .map_err(|err| {
231 log::error!("failed to url-encode auth state: {err}",);
232 api::ErrorCode::InternalError
233 })?;
234
235 let mut url: url::Url = running_state
236 .constellation
237 .auths_url
238 .join(api::auths::YIVI_NEXT_SESSION_PATH)
239 .map_err(|err| {
240 log::error!(
241 "failed to compute authenticatio server's yivi next session url: {err}",
242 );
243 api::ErrorCode::InternalError
244 })?;
245
246 url.set_query(Some(&query));
247
248 dr = dr.next_session(url);
249 }
250
251 dr.sign(&yivi.requestor_creds).into_ec(|err| {
252 log::error!("failed to create signed disclosure request: {err}",);
253 api::ErrorCode::InternalError
254 })?
255 };
256
257 if sealed_state.is_none() {
258 sealed_state = Some(seal_state(&state)?);
259 }
260
261 Ok(api::auths::AuthStartResp::Success {
262 task: api::auths::AuthTask::Yivi {
263 disclosure_request,
264 yivi_requestor_url: yivi.requestor_url.clone(),
265 },
266 state: sealed_state.unwrap(),
267 })
268 }
269
270 pub async fn handle_auth_complete(
271 app: Rc<Self>,
272 req: web::Json<api::auths::AuthCompleteReq>,
273 ) -> api::Result<api::auths::AuthCompleteResp> {
274 app.running_state_or_please_retry()?;
275
276 let req: api::auths::AuthCompleteReq = req.into_inner();
277
278 let Some(state) = AuthState::unseal(&req.state, &app.auth_state_secret) else {
279 return Ok(api::auths::AuthCompleteResp::PleaseRestartAuth);
280 };
281
282 match state.source {
283 attr::Source::Yivi => {
284 Self::handle_auth_complete_yivi(
285 app,
286 state,
287 match req.proof {
288 api::auths::AuthProof::Yivi { disclosure } => disclosure,
289 #[expect(unreachable_patterns)]
290 _ => return Err(api::ErrorCode::BadRequest),
291 },
292 )
293 .await
294 }
295 }
296 }
297
298 async fn handle_auth_complete_yivi(
299 app: Rc<Self>,
300 state: AuthState,
301 disclosure: jwt::JWT,
302 ) -> api::Result<api::auths::AuthCompleteResp> {
303 let yivi = app.get_yivi()?;
304
305 let ssr =
306 yivi::SessionResult::open_signed(&disclosure, &yivi.server_creds).map_err(|err| {
307 log::debug!("invalid yivi signed session result submitted: {err:#}",);
308 api::ErrorCode::BadRequest
309 })?;
310
311 let mut attrs: IndexMap<handle::Handle, api::Signed<attr::Attr>> =
312 IndexMap::with_capacity(state.attr_type_choices.len());
313
314 let running_state = app.running_state_or_internal_error()?;
315
316 for (i, result) in ssr
317 .validate_and_extract_raw_singles()
318 .map_err(|err| {
319 log::debug!("invalid session result submitted: {err}");
320 api::ErrorCode::BadRequest
321 })?
322 .enumerate()
323 {
324 let (yati, raw_value): (&yivi::AttributeTypeIdentifier, &str) =
325 result.map_err(|err| {
326 log::debug!(
327 "problem with attribute number {i} of submitted session result: {err}",
328 );
329 api::ErrorCode::BadRequest
330 })?;
331
332 let Some(ati2at) = state.yivi_ati2at.get(i) else {
333 log::debug!("extra attributes disclosed in submitted session result");
336 return Err(api::ErrorCode::BadRequest);
337 };
338
339 let Some(attr_type_handle) = ati2at.get(yati) else {
340 log::debug!(
341 "got unexpected yivi attribute {yati} at position {i}; expected one of: {}",
342 ati2at
343 .values()
344 .map(handle::Handle::as_str)
345 .collect::<Vec<&str>>()
346 .join(", ")
347 );
348 return Err(api::ErrorCode::BadRequest);
349 };
350
351 let Some(attr_type) = app.attr_type_from_handle(attr_type_handle) else {
352 log::warn!(
353 "Attribute type with handle {attr_type_handle} mentioned in authentication state can no longer be found."
354 );
355 return Ok(api::auths::AuthCompleteResp::PleaseRestartAuth);
356 };
357
358 let old_value = attrs.insert(
361 attr_type_handle.clone(),
362 api::Signed::<attr::Attr>::new(
365 &running_state.attr_signing_key,
366 &attr::Attr {
367 attr_type: attr_type.id,
368 value: raw_value.to_string(),
369 bannable: attr_type.bannable,
370 not_identifying: !attr_type.identifying,
371 not_addable: attr_type.not_addable_by_default,
372 },
373 app.auth_window,
374 )?,
375 );
376
377 if old_value.is_some() {
378 log::error!("expected to have already erred on duplicate attribute types");
379 return Err(api::ErrorCode::InternalError);
380 }
381 }
382
383 Ok(api::auths::AuthCompleteResp::Success { attrs })
384 }
385}