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        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    /// Creates a disclosure 'conjunction' for the given yivi attribute type identifier.
71    ///
72    /// This is almost always just the attibute type idenfitier itself, unless we're dealing with
73    /// the pubhubs card - in which case two other factors are added that fixes the registration
74    /// source, and allows the user to see the 'comment' attached to the card (usually the
75    /// redacted email address and phone number.)
76    ///
77    /// The yivi attribute type that will provide the actual value for the pubhubs attribute
78    /// will always come first.  This is important because
79    /// [`yivi::SessionResult::validate_and_extract_raw_singles`] will only pick the first value
80    /// from each inner conjunction.
81    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        // Create ConDisCon for our attributes
140        let mut cdc: servers::yivi::AttributeConDisCon = Default::default(); // empty
141
142        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                // NOTE: debug! and BadRequest, and not warn! and InternalError,
309                // because clients can swap result JWTs from different yivi sessions
310                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            // Disclosure for attribute is OK.
334
335            let old_value = attrs.insert(
336                attr_type_handle.clone(),
337                // TODO: attr_signing_key is constellation-dependent;  provide a mechanism
338                // for the client to detect constellation change
339                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}