Skip to main content

pubhubs/api/
auths.rs

1//! Additional endpoints provided by the authentication server
2use crate::api::*;
3use crate::attr::Attr;
4use crate::misc::jwt;
5use crate::misc::serde_ext::bytes_wrapper::B64UU;
6use crate::{attr, handle};
7
8use serde::{Deserialize, Serialize};
9
10use std::collections::HashMap;
11
12use indexmap::IndexMap;
13
14use actix_web::http;
15
16/// Called by the global client to get, for example, the list of supported attribute types.
17pub struct WelcomeEP {}
18impl EndpointDetails for WelcomeEP {
19    type RequestType = NoPayload;
20    type ResponseType = Result<WelcomeResp>;
21
22    const METHOD: http::Method = http::Method::GET;
23    const PATH: &'static str = ".ph/welcome";
24}
25
26/// Reponse type for [`WelcomeEP`].
27#[derive(Serialize, Deserialize, Debug, Clone)]
28#[serde(deny_unknown_fields)]
29pub struct WelcomeResp {
30    /// Available attribute types
31    pub attr_types: HashMap<handle::Handle, attr::Type>,
32
33    /// A list of historic values for the duration of the validity of pubhubs cards.
34    ///
35    /// This field is only set when yivi and historic values for
36    /// [`crate::servers::auths::card::CardConfig::valid_for`] are configured.
37    ///
38    /// The list is guaranteed to be ordered by [`HistoricCardValidity::starting_at_timestamp`],
39    /// and is gauranteed to contain an entry with `starting_at_timestamp = 0`.
40    #[serde(default)]
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub card_validity: Option<Vec<HistoricCardValidity>>,
43}
44
45/// Type for [`WelcomeResp::card_validity`]
46#[derive(Serialize, Deserialize, Debug, Clone)]
47#[serde(deny_unknown_fields)]
48pub struct HistoricCardValidity {
49    /// Starting from this timestamp (number of seconds since 1970-01-01 00:00:00Z)...
50    pub starting_at_timestamp: NumericDate,
51
52    /// cards were issued that were valid for this many seconds.
53    pub card_valid_for_secs: u64,
54}
55
56/// Starts the process of obtaining attributes from the authentication server.
57pub struct AuthStartEP {}
58impl EndpointDetails for AuthStartEP {
59    type RequestType = AuthStartReq;
60    type ResponseType = Result<AuthStartResp>;
61
62    const METHOD: http::Method = http::Method::POST;
63    const PATH: &'static str = ".ph/auth/start";
64}
65
66/// Request type for [`AuthStartEP`].
67#[derive(Serialize, Deserialize, Debug, Clone)]
68#[serde(deny_unknown_fields)]
69pub struct AuthStartReq {
70    /// Which source to use (e.g. yivi)
71    pub source: attr::Source,
72
73    /// List of requested attributes.
74    ///
75    /// Can be non-empty if and only if [`AuthStartReq::attr_type_choices`] is empty.
76    /// (Otherwise an [`ErrorCode::BadRequest`] is returned.)
77    #[serde(default)]
78    #[serde(skip_serializing_if = "Vec::is_empty")]
79    pub attr_types: Vec<handle::Handle>,
80
81    /// Like [`AuthStartReq::attr_types`], but allow the user to pick each attribute type from a
82    /// list.  For example, if `attr_type_choices` is  `[[ph_card, email], [phone]]` that means the
83    /// user must disclose either a pubhubs card or an email address, and besides that also a phone
84    /// attribute.  
85    ///
86    /// For [`attr::Source::Yivi`] this results in a 'disjunction' in the disclosure request.
87    ///
88    /// Note that we do not offer the option to disclose either (`phone` + `email`) or `ph_card`,
89    /// because Yivi does not allow `phone` and `email` in an inner conjunction, see
90    /// <https://docs.yivi.app/session-requests#multiple-credential-types-within-inner-conjunctions>.
91    #[serde(default)]
92    #[serde(skip_serializing_if = "Vec::is_empty")]
93    pub attr_type_choices: Vec<Vec<handle::Handle>>,
94
95    /// Only when [`Self::source`] is `attr::Source::Yivi` can this flag be set.
96    /// It makes the [`AuthTask::Yivi::disclosure_request`]  instruct the yivi server to use
97    /// [`YIVI_NEXT_SESSION_PATH`] as next `nextSession` url,
98    /// see [yivi documentaton](https://docs.yivi.app/chained-sessions/),
99    /// making it possible to follow-up the disclosure request with the issuance of a PubHubs card.
100    ///
101    /// This means that before the dislosure result is returned to the frontend,
102    /// the Yivi server will post the disclosure result to the [`YIVI_NEXT_SESSION_PATH`] endpoint
103    /// to determine what session to run next.
104    ///
105    /// Upon receipt of the disclosure result the [`YIVI_NEXT_SESSION_PATH`] endpoint will immediately
106    /// make it available via the [`YiviWaitForResultEP`] endpoint, while keeping the Yivi server waiting
107    /// for a response which must be provided via the  [`YiviReleaseNextSessionEP`] endpoint.
108    ///
109    /// This gives the global client time to obtain a PubHubs card issuance request from PubHubs central,
110    /// to be passed to the Yivi server as next session via [`YiviReleaseNextSessionEP`].
111    #[serde(default)]
112    #[serde(skip_serializing_if = "std::ops::Not::not")]
113    pub yivi_chained_session: bool,
114
115    /// Whether to slowly feed the waiting yivi server spaces `" "`, so we can detect when a Yivi
116    /// server disconnects.  When `yivi_chained_session_drip` is enabled, we must immediately
117    /// return a status code to the Yivi server, and so [`YiviReleaseNextSessionReq::next_session`] can
118    /// not be `None`.  This means that using drip we commit to having a next session (e.g. issuing
119    /// a PubHubs card).
120    #[serde(default)]
121    #[serde(skip_serializing_if = "std::ops::Not::not")]
122    pub yivi_chained_session_drip: bool,
123}
124
125/// Response to [`AuthStartEP`]
126#[derive(Serialize, Deserialize, Debug, Clone)]
127#[serde(deny_unknown_fields)]
128#[must_use]
129pub enum AuthStartResp {
130    /// Authentication process was started
131    Success {
132        /// Task for the global client to satisfy the authentication server.
133        /// Depends on the requested attribute types
134        task: AuthTask,
135
136        /// Opaque state that should be sent with the [`AuthCompleteReq`].
137        state: AuthState,
138    },
139
140    /// No attribute type known with this handle
141    UnknownAttrType(handle::Handle),
142
143    /// The [`AuthStartReq::source`] is not available for the attribute type with this handle
144    SourceNotAvailableFor(handle::Handle),
145
146    /// For some reason these two attribute types cannot be requested together
147    ///
148    /// For [`attr::Source::Yivi`] this might happen if the two attribute types can be derived from
149    /// the same [`crate::servers::yivi::AttributeTypeIdentifier`]. This is not something that is currently e
150    /// expected to happen.
151    Conflict(handle::Handle, handle::Handle),
152}
153
154#[derive(Serialize, Deserialize, Debug, Clone)]
155#[serde(transparent)]
156pub struct AuthState {
157    pub(crate) inner: B64UU,
158}
159
160impl AuthState {
161    pub(crate) fn new(inner: serde_bytes::ByteBuf) -> Self {
162        Self {
163            inner: inner.into(),
164        }
165    }
166}
167
168impl std::fmt::Display for AuthState {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        self.inner.fmt(f)
171    }
172}
173
174#[derive(Serialize, Deserialize, Debug, Clone)]
175#[serde(deny_unknown_fields)]
176pub enum AuthTask {
177    /// Have the end-user disclose to the specified yivi server.
178    /// The authentication server only creates the signed (disclosure) session request,
179    /// but it's up to the global client to send it to the yivi server.
180    Yivi {
181        disclosure_request: jwt::JWT,
182        yivi_requestor_url: url::Url,
183    },
184}
185
186/// After having completed the task set by the authentication server,
187/// obtain the attributes.
188pub struct AuthCompleteEP {}
189impl EndpointDetails for AuthCompleteEP {
190    type RequestType = AuthCompleteReq;
191    type ResponseType = Result<AuthCompleteResp>;
192
193    const METHOD: http::Method = http::Method::POST;
194    const PATH: &'static str = ".ph/auth/complete";
195}
196
197#[derive(Serialize, Deserialize, Debug, Clone)]
198#[serde(deny_unknown_fields)]
199pub struct AuthCompleteReq {
200    /// Proof that the end-user possesses the requested attributes.
201    pub proof: AuthProof,
202
203    /// The [`AuthStartResp::Success::state`] obtained earlier.
204    pub state: AuthState,
205}
206
207#[derive(Serialize, Deserialize, Debug, Clone)]
208#[serde(deny_unknown_fields)]
209pub enum AuthProof {
210    Yivi {
211        /// The JWT returned by the yivi server's `/session/(...)/result-jwt` after completing a session
212        /// with [`AuthTask::Yivi::disclosure_request`].
213        disclosure: jwt::JWT,
214    },
215}
216
217#[derive(Serialize, Deserialize, Debug, Clone)]
218#[serde(deny_unknown_fields)]
219#[must_use]
220pub enum AuthCompleteResp {
221    /// All went well
222    Success {
223        /// The resulting attributes, in the same order they were requested in
224        /// [`AuthStartReq::attr_types`] or [`AuthStartReq::attr_type_choices`].
225        /// In the latter case, the key indicates the choice the user made.
226        attrs: IndexMap<handle::Handle, Signed<Attr>>,
227    },
228
229    /// Something went wrong;  please start again at [`AuthStartEP`].
230    ///
231    /// One reason is that the authentication server restarted and that the provided authenication
232    /// state is no longer valid.
233    PleaseRestartAuth,
234}
235
236/// Allows the global client to retrieve secrets tied to identifying [`Attr`]ibutes.
237///
238/// These *attribute keys* are used by the global client to encrypt its  *master secret(s)* before
239/// storing them at pubhubs central.
240///
241/// To allow a compromised attribute key to be replaced (automatically), attribute keys are tied
242/// not only to an attribute, but also a timestamp.
243pub struct AttrKeysEP {}
244impl EndpointDetails for AttrKeysEP {
245    type RequestType = HashMap<handle::Handle, AttrKeyReq>;
246    type ResponseType = Result<AttrKeysResp>;
247
248    const METHOD: http::Method = http::Method::POST;
249    const PATH: &'static str = ".ph/attr-keys";
250}
251
252/// Request type for [`AttrKeysEP`]
253#[derive(Serialize, Deserialize, Debug, Clone)]
254#[serde(deny_unknown_fields)]
255pub struct AttrKeyReq {
256    /// A signed attribute, obtained via [`AuthCompleteEP`].
257    ///
258    /// The attribute must be identifying.
259    pub attr: Signed<Attr>,
260
261    /// If set, will not only return the latest attribute key for `attr`, but also an older
262    /// attribute key tied to the given timestamp.
263    pub timestamp: Option<NumericDate>,
264}
265
266/// Response type for [`AttrKeysEP`]
267#[derive(Serialize, Deserialize, Debug, Clone)]
268#[serde(deny_unknown_fields)]
269#[must_use]
270pub enum AttrKeysResp {
271    /// The attribute with the given handle is not (or no longer) valid.  Reobtain the attribute
272    /// and try again.
273    RetryWithNewAttr(handle::Handle),
274
275    /// Successfully retrieves keys for all attributes provided.
276    Success(HashMap<handle::Handle, AttrKeyResp>),
277}
278
279/// Part of a successful [`AttrKeyResp`].
280#[derive(Serialize, Deserialize, Debug, Clone)]
281#[serde(deny_unknown_fields)]
282#[must_use]
283pub struct AttrKeyResp {
284    /// A pair, `(key, timestamp)`, where `key` is the latest attribute key for the requested attribute
285    /// and `timestamp` can be used to retrieve the same key again later on by setting `AttrKeyReq::timestamp`.
286    pub latest_key: (B64UU, NumericDate),
287
288    /// The attribute key at [`AttrKeyReq::timestamp`], when this was set.
289    ///
290    /// This key should only be use for decryption, not for encryption.
291    pub old_key: Option<B64UU>,
292}
293
294/// Request a pubhubs card, or rather, a signed session request for the issuance of a pubhubs card
295/// that can be passed to the authentication server's yivi server to actually issue the card.
296pub struct CardEP {}
297impl EndpointDetails for CardEP {
298    type RequestType = CardReq;
299    type ResponseType = Result<CardResp>;
300
301    const METHOD: http::Method = http::Method::POST;
302    const PATH: &'static str = ".ph/card";
303}
304
305/// Request type for [`CardEP`]
306#[derive(Serialize, Deserialize, Debug, Clone)]
307#[serde(deny_unknown_fields)]
308#[must_use]
309pub struct CardReq {
310    /// A by PHC signed registration pseudonym obtained via [`phc::user::CardPseudEP`].
311    pub card_pseud_package: Signed<phc::user::CardPseudPackage>,
312
313    /// Optional comment used after the registration date field.
314    /// Could perhaps be the partly anonymized email address and phone number used to originally
315    /// register this account.
316    pub comment: Option<String>,
317}
318
319/// What's returned by [`CardEP`]
320#[derive(Serialize, Deserialize, Debug, Clone)]
321#[serde(deny_unknown_fields)]
322#[must_use]
323pub enum CardResp {
324    Success {
325        /// Attribute for the card that can be added to the user's account via the
326        /// [`phc::user::EnterEP`] endpoint.
327        ///
328        /// Make sure that you first add this attribute to the user's account before you add the
329        /// card to the user's yivi app - we do not want to end up with a card in the yivi app that
330        /// is not connected to the user's account.
331        attr: Signed<Attr>,
332
333        /// Signed issuance request to issue the pubhubs card.  Can be used to start a new session
334        /// with the Yivi server directly, or an already existing chained session with the
335        /// authentication server, via [`YiviReleaseNextSessionEP`].
336        ///
337        /// Before making the issuance request, make sure [`CardResp::Success::attr`] is added to the
338        /// user's account!
339        issuance_request: jwt::JWT,
340
341        /// The Yivi server that can handle the issuance request
342        yivi_requestor_url: url::Url,
343    },
344
345    /// Please try again with a new signed card pseudonym package
346    PleaseRetryWithNewCardPseud,
347}
348
349/// Wait for the disclosure result that the yivi server will post to the authentication server
350///
351/// Might return [`ErrorCode::BadRequest`] when yivi is not configured for this authentication
352/// server, or when [`AuthStartReq::yivi_chained_session`] was not set for [`YiviWaitForResultReq::state`].
353pub struct YiviWaitForResultEP {}
354impl EndpointDetails for YiviWaitForResultEP {
355    type RequestType = YiviWaitForResultReq;
356    type ResponseType = Result<YiviWaitForResultResp>;
357
358    const METHOD: http::Method = http::Method::POST;
359    const PATH: &'static str = ".ph/yivi/wait-for-result";
360}
361
362/// Request type for [`YiviWaitForResultEP`]
363#[derive(Serialize, Deserialize, Debug, Clone)]
364#[serde(deny_unknown_fields)]
365#[must_use]
366pub struct YiviWaitForResultReq {
367    /// The [`AuthStartResp::Success::state`] returned earlier
368    pub state: AuthState,
369}
370
371/// What's returned by [`YiviWaitForResultEP`]
372#[derive(Serialize, Deserialize, Debug, Clone)]
373#[serde(deny_unknown_fields)]
374#[must_use]
375pub enum YiviWaitForResultResp {
376    Success {
377        /// The disclosure result posted by the Yivi server
378        disclosure: jwt::JWT,
379    },
380
381    /// Something went wrong;  please start again at [`AuthStartEP`].
382    ///
383    /// One reason is that the authentication server restarted and that the provided authenication
384    /// state is no longer valid.
385    PleaseRestartAuth,
386
387    /// The request seems fine, but the session cannot be found.  Either the session expired, or
388    /// was already completed.  Could caused by a logic error in the client, but also by a slow
389    /// internet connection.
390    SessionGone,
391}
392
393/// Provide the waiting yivi server with the next session.
394pub struct YiviReleaseNextSessionEP {}
395impl EndpointDetails for YiviReleaseNextSessionEP {
396    type RequestType = YiviReleaseNextSessionReq;
397    type ResponseType = Result<YiviReleaseNextSessionResp>;
398
399    const METHOD: http::Method = http::Method::POST;
400    const PATH: &'static str = ".ph/yivi/release-next-session";
401}
402
403/// Request type for [`YiviReleaseNextSessionEP`]
404#[derive(Serialize, Deserialize, Debug, Clone)]
405#[serde(deny_unknown_fields)]
406#[must_use]
407pub struct YiviReleaseNextSessionReq {
408    /// The [`AuthStartResp::Success::state`] returned earlier
409    pub state: AuthState,
410
411    /// Instructs the authentication server on what next session (if any) to start at the yivi server.
412    ///
413    /// If `None` the yivi server will be served a `HTTP 204` causing it to stop the yivi flow
414    /// normally without opening a follow-up session.
415    ///
416    /// Otherwise it must be some signed session request that will be passed to yivi server.
417    /// This session request must be signed by the authentication server's yivi requestor credentials,
418    /// for example, [`CardResp::Success::issuance_request`].
419    ///
420    /// The value `None` is not permitted when [`AuthStartReq::yivi_chained_session_drip`]
421    /// was set (because in that case the HTTP status code `200` has already been sent to the Yivi server.)
422    /// If `None` is submitted anyway, this causes an `ErrorCode::BadRequest`.
423    pub next_session: Option<jwt::JWT>,
424
425    /// If the yivi server has been waiting more than this amount of milliseconds when it is about to be released,
426    /// dont release it, but ghost it.
427    /// The reason is that drip mechanism may be too slow to detect the yivi server timing out.
428    #[serde(default)]
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub stale_after: Option<u16>,
431}
432
433/// What's returned by [`YiviReleaseNextSessionEP`]
434#[derive(Serialize, Deserialize, Debug, Clone)]
435#[serde(deny_unknown_fields)]
436#[must_use]
437pub enum YiviReleaseNextSessionResp {
438    Success {},
439
440    /// Something went wrong;  please start again at [`AuthStartEP`].
441    ///
442    /// One reason is that the authentication server restarted and that the provided authenication
443    /// state is no longer valid.
444    PleaseRestartAuth,
445
446    /// The request seems fine, but the session cannot be found.  Either the session expired, or
447    /// was already completed.  Could caused by a logic error in the client, but also by a slow
448    /// internet connection.
449    SessionGone,
450
451    /// The request seems fine, but the Yivi server is gone, perhaps because it timed out.
452    /// Also returned when the authentication server deems the yivi server stale, see [`YiviReleaseNextSessionReq::stale_after`].
453    YiviServerGone,
454
455    /// Trying to release a yivi server that's not there yet.  You should first call the
456    /// [`YiviWaitForResultEP`] endpoint to make sure the yivi server is there.
457    TooEarly,
458}
459
460/// Path for the endpoint used by the yivi server to get the next session in a chained session.
461///
462/// Note that this endpoint does not conform to the [`EndpointDetails`] format, using, for example,
463/// the HTTP status code to convey information (`204` means no next session).
464pub const YIVI_NEXT_SESSION_PATH: &str = ".ph/yivi/next-session";
465
466/// Query parameters to the [`YIVI_NEXT_SESSION_PATH`] endpoint.
467#[derive(Serialize, Deserialize, Debug, Clone)]
468#[serde(deny_unknown_fields)]
469#[must_use]
470pub struct YiviNextSessionQuery {
471    pub state: AuthState,
472}