Skip to main content

pubhubs/servers/auths/
auth.rs

1//! Implementation of the [`api::auths::AuthStartEP`] and [`api::auths::AuthCompleteEP`] endpoints.
2use 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
17/// # Implementaton of endpoints
18impl App {
19    /// Implements [`api::auths::AuthStartEP`]
20    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        // Bound the work (and the size of the signed disclosure request and sealed state) that an
44        // anonymous request can trigger; a real request asks for only a few attribute types.
45        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    /// Creates a disclosure 'conjunction' for the given yivi attribute type identifier.
89    ///
90    /// This is almost always just the attibute type idenfitier itself, unless we're dealing with
91    /// the pubhubs card - in which case two other factors are added that fixes the registration
92    /// source, and allows the user to see the 'comment' attached to the card (usually the
93    /// redacted email address and phone number.)
94    ///
95    /// The yivi attribute type that will provide the actual value for the pubhubs attribute
96    /// will always come first.  This is important because
97    /// [`yivi::SessionResult::validate_and_extract_raw_singles`] will only pick the first value
98    /// from each inner conjunction.
99    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        // Create ConDisCon for our attributes
158        let mut cdc: servers::yivi::AttributeConDisCon = Default::default(); // empty
159
160        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                // NOTE: debug! and BadRequest, and not warn! and InternalError,
334                // because clients can swap result JWTs from different yivi sessions
335                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            // Disclosure for attribute is OK.
359
360            let old_value = attrs.insert(
361                attr_type_handle.clone(),
362                // TODO: attr_signing_key is constellation-dependent;  provide a mechanism
363                // for the client to detect constellation change
364                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}