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 attr_type_choices = if req.attr_type_choices.is_empty() {
44 req.attr_types.into_iter().map(|at| vec![at]).collect()
45 } else {
46 req.attr_type_choices
47 };
48
49 let state = AuthState {
50 source: req.source,
51 attr_type_choices,
52 exp: api::NumericDate::now() + app.auth_window,
53 yivi_chained_session: None,
54 yivi_ati2at: Default::default(),
55 };
56
57 match req.source {
58 attr::Source::Yivi => {
59 Self::handle_auth_start_yivi(
60 app,
61 state,
62 req.yivi_chained_session,
63 req.yivi_chained_session_drip,
64 )
65 .await
66 }
67 }
68 }
69
70 fn create_disclosure_con_for(
82 &self,
83 attr_type_id: &servers::yivi::AttributeTypeIdentifier,
84 ) -> api::Result<Vec<servers::yivi::AttributeRequest>> {
85 let yivi = self.get_yivi()?;
86
87 let mut result = vec![servers::yivi::AttributeRequest {
88 ty: attr_type_id.clone(),
89 value: None,
90 }];
91
92 let credential = yivi.card_config.card_type.credential();
93
94 if !attr_type_id.as_str().starts_with(credential) {
95 return Ok(result);
96 }
97
98 let registration_date = yivi.card_config.card_type.date();
99
100 result.push(servers::yivi::AttributeRequest {
101 ty: format!("{credential}.{registration_date}")
102 .parse()
103 .map_err(|err| {
104 log::error!("failed to form registration date yivi attribute: {err:?}");
105 api::ErrorCode::InternalError
106 })?,
107 value: None,
108 });
109
110 let registration_source = yivi.card_config.card_type.source();
111
112 result.push(servers::yivi::AttributeRequest {
113 ty: format!("{credential}.{registration_source}")
114 .parse()
115 .map_err(|err| {
116 log::error!("failed to form registration source yivi attribute: {err:?}");
117 api::ErrorCode::InternalError
118 })?,
119 value: Some(self.registration_source(yivi).to_owned()),
120 });
121
122 Ok(result)
123 }
124
125 async fn handle_auth_start_yivi(
126 app: Rc<Self>,
127 mut state: AuthState,
128 yivi_chained_session: bool,
129 yivi_chained_session_drip: bool,
130 ) -> api::Result<api::auths::AuthStartResp> {
131 let yivi = app.get_yivi()?;
132
133 let mut sealed_state: Option<api::auths::AuthState> = None;
134
135 let seal_state = |state: &AuthState| -> api::Result<api::auths::AuthState> {
136 state.seal(&app.auth_state_secret)
137 };
138
139 let mut cdc: servers::yivi::AttributeConDisCon = Default::default(); for attr_ty_options in state.attr_type_choices.iter() {
143 let mut dc: Vec<Vec<servers::yivi::AttributeRequest>> = Default::default();
144 let mut ati2at: HashMap<yivi::AttributeTypeIdentifier, handle::Handle> =
145 Default::default();
146
147 for attr_ty_handle in attr_ty_options.iter() {
148 let Some(attr_ty) = app.attr_type_from_handle(attr_ty_handle) else {
149 return Ok(api::auths::AuthStartResp::UnknownAttrType(
150 attr_ty_handle.clone(),
151 ));
152 };
153
154 let mut had_one: bool = false;
155
156 for ati in attr_ty.yivi_attr_type_ids() {
157 if let Some(existing_at_handle) =
158 ati2at.insert(ati.clone(), attr_ty_handle.clone())
159 {
160 log::debug!(
161 "attribute types {existing_at_handle} and {attr_ty_handle} both rely on the same yivi attribute type identifier {ati}"
162 );
163 return Ok(api::auths::AuthStartResp::Conflict(
164 existing_at_handle,
165 attr_ty_handle.clone(),
166 ));
167 }
168
169 had_one = true;
170
171 dc.push(app.create_disclosure_con_for(ati)?);
172 }
173
174 if !had_one {
175 log::debug!(
176 "got yivi authentication start request for {attr_ty_handle}, but yivi is not supported for this attribute type",
177 );
178 return Ok(api::auths::AuthStartResp::SourceNotAvailableFor(
179 attr_ty_handle.clone(),
180 ));
181 }
182 }
183
184 state.yivi_ati2at.push(ati2at);
185 cdc.push(dc);
186 }
187
188 let disclosure_request: jwt::JWT = {
189 let mut dr = servers::yivi::ExtendedSessionRequest::disclosure(cdc);
190
191 if yivi_chained_session {
192 let csc = app.chained_sessions_ctl_or_bad_request()?;
193 let running_state = app.running_state_or_internal_error()?;
194
195 state.yivi_chained_session = Some(ChainedSessionSetup {
196 id: csc.create_session().await?,
197 drip: yivi_chained_session_drip,
198 });
199
200 sealed_state = Some(seal_state(&state)?);
201
202 let query = serde_urlencoded::to_string(api::auths::YiviNextSessionQuery {
203 state: sealed_state.as_ref().unwrap().clone(),
204 })
205 .map_err(|err| {
206 log::error!("failed to url-encode auth state: {err}",);
207 api::ErrorCode::InternalError
208 })?;
209
210 let mut url: url::Url = running_state
211 .constellation
212 .auths_url
213 .join(api::auths::YIVI_NEXT_SESSION_PATH)
214 .map_err(|err| {
215 log::error!(
216 "failed to compute authenticatio server's yivi next session url: {err}",
217 );
218 api::ErrorCode::InternalError
219 })?;
220
221 url.set_query(Some(&query));
222
223 dr = dr.next_session(url);
224 }
225
226 dr.sign(&yivi.requestor_creds).into_ec(|err| {
227 log::error!("failed to create signed disclosure request: {err}",);
228 api::ErrorCode::InternalError
229 })?
230 };
231
232 if sealed_state.is_none() {
233 sealed_state = Some(seal_state(&state)?);
234 }
235
236 Ok(api::auths::AuthStartResp::Success {
237 task: api::auths::AuthTask::Yivi {
238 disclosure_request,
239 yivi_requestor_url: yivi.requestor_url.clone(),
240 },
241 state: sealed_state.unwrap(),
242 })
243 }
244
245 pub async fn handle_auth_complete(
246 app: Rc<Self>,
247 req: web::Json<api::auths::AuthCompleteReq>,
248 ) -> api::Result<api::auths::AuthCompleteResp> {
249 app.running_state_or_please_retry()?;
250
251 let req: api::auths::AuthCompleteReq = req.into_inner();
252
253 let Some(state) = AuthState::unseal(&req.state, &app.auth_state_secret) else {
254 return Ok(api::auths::AuthCompleteResp::PleaseRestartAuth);
255 };
256
257 match state.source {
258 attr::Source::Yivi => {
259 Self::handle_auth_complete_yivi(
260 app,
261 state,
262 match req.proof {
263 api::auths::AuthProof::Yivi { disclosure } => disclosure,
264 #[expect(unreachable_patterns)]
265 _ => return Err(api::ErrorCode::BadRequest),
266 },
267 )
268 .await
269 }
270 }
271 }
272
273 async fn handle_auth_complete_yivi(
274 app: Rc<Self>,
275 state: AuthState,
276 disclosure: jwt::JWT,
277 ) -> api::Result<api::auths::AuthCompleteResp> {
278 let yivi = app.get_yivi()?;
279
280 let ssr =
281 yivi::SessionResult::open_signed(&disclosure, &yivi.server_creds).map_err(|err| {
282 log::debug!("invalid yivi signed session result submitted: {err:#}",);
283 api::ErrorCode::BadRequest
284 })?;
285
286 let mut attrs: IndexMap<handle::Handle, api::Signed<attr::Attr>> =
287 IndexMap::with_capacity(state.attr_type_choices.len());
288
289 let running_state = app.running_state_or_internal_error()?;
290
291 for (i, result) in ssr
292 .validate_and_extract_raw_singles()
293 .map_err(|err| {
294 log::debug!("invalid session result submitted: {err}");
295 api::ErrorCode::BadRequest
296 })?
297 .enumerate()
298 {
299 let (yati, raw_value): (&yivi::AttributeTypeIdentifier, &str) =
300 result.map_err(|err| {
301 log::debug!(
302 "problem with attribute number {i} of submitted session result: {err}",
303 );
304 api::ErrorCode::BadRequest
305 })?;
306
307 let Some(ati2at) = state.yivi_ati2at.get(i) else {
308 log::debug!("extra attributes disclosed in submitted session result");
311 return Err(api::ErrorCode::BadRequest);
312 };
313
314 let Some(attr_type_handle) = ati2at.get(yati) else {
315 log::debug!(
316 "got unexpected yivi attribute {yati} at position {i}; expected one of: {}",
317 ati2at
318 .values()
319 .map(handle::Handle::as_str)
320 .collect::<Vec<&str>>()
321 .join(", ")
322 );
323 return Err(api::ErrorCode::BadRequest);
324 };
325
326 let Some(attr_type) = app.attr_type_from_handle(attr_type_handle) else {
327 log::warn!(
328 "Attribute type with handle {attr_type_handle} mentioned in authentication state can no longer be found."
329 );
330 return Ok(api::auths::AuthCompleteResp::PleaseRestartAuth);
331 };
332
333 let old_value = attrs.insert(
336 attr_type_handle.clone(),
337 api::Signed::<attr::Attr>::new(
340 &running_state.attr_signing_key,
341 &attr::Attr {
342 attr_type: attr_type.id,
343 value: raw_value.to_string(),
344 bannable: attr_type.bannable,
345 not_identifying: !attr_type.identifying,
346 not_addable: attr_type.not_addable_by_default,
347 },
348 app.auth_window,
349 )?,
350 );
351
352 if old_value.is_some() {
353 log::error!("expected to have already erred on duplicate attribute types");
354 return Err(api::ErrorCode::InternalError);
355 }
356 }
357
358 Ok(api::auths::AuthCompleteResp::Success { attrs })
359 }
360}