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 /// The authentication server is temporarily at its configured maximum number of concurrent
154 /// chained sessions, so it cannot start a new one right now.
155 ///
156 /// The global client may fall back to the non-chained flow by retrying [`AuthStartEP`] with
157 /// `yivi_chained_session` (and `yivi_chained_session_drip`) set to `false`, at the cost of the
158 /// user scanning a second Yivi QR for card issuance.
159 ChainedSessionsTemporarilyUnavailable,
160}
161
162#[derive(Serialize, Deserialize, Debug, Clone)]
163#[serde(transparent)]
164pub struct AuthState {
165 pub(crate) inner: B64UU,
166}
167
168impl AuthState {
169 pub(crate) fn new(inner: serde_bytes::ByteBuf) -> Self {
170 Self {
171 inner: inner.into(),
172 }
173 }
174}
175
176impl std::fmt::Display for AuthState {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 self.inner.fmt(f)
179 }
180}
181
182#[derive(Serialize, Deserialize, Debug, Clone)]
183#[serde(deny_unknown_fields)]
184pub enum AuthTask {
185 /// Have the end-user disclose to the specified yivi server.
186 /// The authentication server only creates the signed (disclosure) session request,
187 /// but it's up to the global client to send it to the yivi server.
188 Yivi {
189 disclosure_request: jwt::JWT,
190 yivi_requestor_url: url::Url,
191 },
192}
193
194/// After having completed the task set by the authentication server,
195/// obtain the attributes.
196pub struct AuthCompleteEP {}
197impl EndpointDetails for AuthCompleteEP {
198 type RequestType = AuthCompleteReq;
199 type ResponseType = Result<AuthCompleteResp>;
200
201 const METHOD: http::Method = http::Method::POST;
202 const PATH: &'static str = ".ph/auth/complete";
203}
204
205#[derive(Serialize, Deserialize, Debug, Clone)]
206#[serde(deny_unknown_fields)]
207pub struct AuthCompleteReq {
208 /// Proof that the end-user possesses the requested attributes.
209 pub proof: AuthProof,
210
211 /// The [`AuthStartResp::Success::state`] obtained earlier.
212 pub state: AuthState,
213}
214
215#[derive(Serialize, Deserialize, Debug, Clone)]
216#[serde(deny_unknown_fields)]
217pub enum AuthProof {
218 Yivi {
219 /// The JWT returned by the yivi server's `/session/(...)/result-jwt` after completing a session
220 /// with [`AuthTask::Yivi::disclosure_request`].
221 disclosure: jwt::JWT,
222 },
223}
224
225#[derive(Serialize, Deserialize, Debug, Clone)]
226#[serde(deny_unknown_fields)]
227#[must_use]
228pub enum AuthCompleteResp {
229 /// All went well
230 Success {
231 /// The resulting attributes, in the same order they were requested in
232 /// [`AuthStartReq::attr_types`] or [`AuthStartReq::attr_type_choices`].
233 /// In the latter case, the key indicates the choice the user made.
234 attrs: IndexMap<handle::Handle, Signed<Attr>>,
235 },
236
237 /// Something went wrong; please start again at [`AuthStartEP`].
238 ///
239 /// One reason is that the authentication server restarted and that the provided authenication
240 /// state is no longer valid.
241 PleaseRestartAuth,
242}
243
244/// Allows the global client to retrieve secrets tied to identifying [`Attr`]ibutes.
245///
246/// These *attribute keys* are used by the global client to encrypt its *master secret(s)* before
247/// storing them at pubhubs central.
248///
249/// To allow a compromised attribute key to be replaced (automatically), attribute keys are tied
250/// not only to an attribute, but also a timestamp.
251pub struct AttrKeysEP {}
252impl EndpointDetails for AttrKeysEP {
253 type RequestType = HashMap<handle::Handle, AttrKeyReq>;
254 type ResponseType = Result<AttrKeysResp>;
255
256 const METHOD: http::Method = http::Method::POST;
257 const PATH: &'static str = ".ph/attr-keys";
258}
259
260/// Request type for [`AttrKeysEP`]
261#[derive(Serialize, Deserialize, Debug, Clone)]
262#[serde(deny_unknown_fields)]
263pub struct AttrKeyReq {
264 /// A signed attribute, obtained via [`AuthCompleteEP`].
265 ///
266 /// The attribute must be identifying.
267 pub attr: Signed<Attr>,
268
269 /// If set, will not only return the latest attribute key for `attr`, but also an older
270 /// attribute key tied to the given timestamp.
271 pub timestamp: Option<NumericDate>,
272}
273
274/// Response type for [`AttrKeysEP`]
275#[derive(Serialize, Deserialize, Debug, Clone)]
276#[serde(deny_unknown_fields)]
277#[must_use]
278pub enum AttrKeysResp {
279 /// The attribute with the given handle is not (or no longer) valid. Reobtain the attribute
280 /// and try again.
281 RetryWithNewAttr(handle::Handle),
282
283 /// Successfully retrieves keys for all attributes provided.
284 Success(HashMap<handle::Handle, AttrKeyResp>),
285}
286
287/// Part of a successful [`AttrKeyResp`].
288#[derive(Serialize, Deserialize, Debug, Clone)]
289#[serde(deny_unknown_fields)]
290#[must_use]
291pub struct AttrKeyResp {
292 /// A pair, `(key, timestamp)`, where `key` is the latest attribute key for the requested attribute
293 /// and `timestamp` can be used to retrieve the same key again later on by setting `AttrKeyReq::timestamp`.
294 pub latest_key: (B64UU, NumericDate),
295
296 /// The attribute key at [`AttrKeyReq::timestamp`], when this was set.
297 ///
298 /// This key should only be use for decryption, not for encryption.
299 pub old_key: Option<B64UU>,
300}
301
302/// Request a pubhubs card, or rather, a signed session request for the issuance of a pubhubs card
303/// that can be passed to the authentication server's yivi server to actually issue the card.
304pub struct CardEP {}
305impl EndpointDetails for CardEP {
306 type RequestType = CardReq;
307 type ResponseType = Result<CardResp>;
308
309 const METHOD: http::Method = http::Method::POST;
310 const PATH: &'static str = ".ph/card";
311}
312
313/// Request type for [`CardEP`]
314#[derive(Serialize, Deserialize, Debug, Clone)]
315#[serde(deny_unknown_fields)]
316#[must_use]
317pub struct CardReq {
318 /// A by PHC signed registration pseudonym obtained via [`phc::user::CardPseudEP`].
319 pub card_pseud_package: Signed<phc::user::CardPseudPackage>,
320
321 /// Optional comment used after the registration date field.
322 /// Could perhaps be the partly anonymized email address and phone number used to originally
323 /// register this account.
324 pub comment: Option<String>,
325}
326
327/// What's returned by [`CardEP`]
328#[derive(Serialize, Deserialize, Debug, Clone)]
329#[serde(deny_unknown_fields)]
330#[must_use]
331pub enum CardResp {
332 Success {
333 /// Attribute for the card that can be added to the user's account via the
334 /// [`phc::user::EnterEP`] endpoint.
335 ///
336 /// Make sure that you first add this attribute to the user's account before you add the
337 /// card to the user's yivi app - we do not want to end up with a card in the yivi app that
338 /// is not connected to the user's account.
339 attr: Signed<Attr>,
340
341 /// Signed issuance request to issue the pubhubs card. Can be used to start a new session
342 /// with the Yivi server directly, or an already existing chained session with the
343 /// authentication server, via [`YiviReleaseNextSessionEP`].
344 ///
345 /// Before making the issuance request, make sure [`CardResp::Success::attr`] is added to the
346 /// user's account!
347 issuance_request: jwt::JWT,
348
349 /// The Yivi server that can handle the issuance request
350 yivi_requestor_url: url::Url,
351 },
352
353 /// Please try again with a new signed card pseudonym package
354 PleaseRetryWithNewCardPseud,
355}
356
357/// Wait for the disclosure result that the yivi server will post to the authentication server
358///
359/// Might return [`ErrorCode::BadRequest`] when yivi is not configured for this authentication
360/// server, or when [`AuthStartReq::yivi_chained_session`] was not set for [`YiviWaitForResultReq::state`].
361pub struct YiviWaitForResultEP {}
362impl EndpointDetails for YiviWaitForResultEP {
363 type RequestType = YiviWaitForResultReq;
364 type ResponseType = Result<YiviWaitForResultResp>;
365
366 const METHOD: http::Method = http::Method::POST;
367 const PATH: &'static str = ".ph/yivi/wait-for-result";
368}
369
370/// Request type for [`YiviWaitForResultEP`]
371#[derive(Serialize, Deserialize, Debug, Clone)]
372#[serde(deny_unknown_fields)]
373#[must_use]
374pub struct YiviWaitForResultReq {
375 /// The [`AuthStartResp::Success::state`] returned earlier
376 pub state: AuthState,
377}
378
379/// What's returned by [`YiviWaitForResultEP`]
380#[derive(Serialize, Deserialize, Debug, Clone)]
381#[serde(deny_unknown_fields)]
382#[must_use]
383pub enum YiviWaitForResultResp {
384 Success {
385 /// The disclosure result posted by the Yivi server
386 disclosure: jwt::JWT,
387 },
388
389 /// Something went wrong; please start again at [`AuthStartEP`].
390 ///
391 /// One reason is that the authentication server restarted and that the provided authenication
392 /// state is no longer valid.
393 PleaseRestartAuth,
394
395 /// The request seems fine, but the session cannot be found. Either the session expired, or
396 /// was already completed. Could caused by a logic error in the client, but also by a slow
397 /// internet connection.
398 SessionGone,
399}
400
401/// Provide the waiting yivi server with the next session.
402pub struct YiviReleaseNextSessionEP {}
403impl EndpointDetails for YiviReleaseNextSessionEP {
404 type RequestType = YiviReleaseNextSessionReq;
405 type ResponseType = Result<YiviReleaseNextSessionResp>;
406
407 const METHOD: http::Method = http::Method::POST;
408 const PATH: &'static str = ".ph/yivi/release-next-session";
409}
410
411/// Request type for [`YiviReleaseNextSessionEP`]
412#[derive(Serialize, Deserialize, Debug, Clone)]
413#[serde(deny_unknown_fields)]
414#[must_use]
415pub struct YiviReleaseNextSessionReq {
416 /// The [`AuthStartResp::Success::state`] returned earlier
417 pub state: AuthState,
418
419 /// Instructs the authentication server on what next session (if any) to start at the yivi server.
420 ///
421 /// If `None` the yivi server will be served a `HTTP 204` causing it to stop the yivi flow
422 /// normally without opening a follow-up session.
423 ///
424 /// Otherwise it must be some signed session request that will be passed to yivi server.
425 /// This session request must be signed by the authentication server's yivi requestor credentials,
426 /// for example, [`CardResp::Success::issuance_request`].
427 ///
428 /// The value `None` is not permitted when [`AuthStartReq::yivi_chained_session_drip`]
429 /// was set (because in that case the HTTP status code `200` has already been sent to the Yivi server.)
430 /// If `None` is submitted anyway, this causes an `ErrorCode::BadRequest`.
431 pub next_session: Option<jwt::JWT>,
432
433 /// If the yivi server has been waiting more than this amount of milliseconds when it is about to be released,
434 /// dont release it, but ghost it.
435 /// The reason is that drip mechanism may be too slow to detect the yivi server timing out.
436 #[serde(default)]
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub stale_after: Option<u16>,
439}
440
441/// What's returned by [`YiviReleaseNextSessionEP`]
442#[derive(Serialize, Deserialize, Debug, Clone)]
443#[serde(deny_unknown_fields)]
444#[must_use]
445pub enum YiviReleaseNextSessionResp {
446 Success {},
447
448 /// Something went wrong; please start again at [`AuthStartEP`].
449 ///
450 /// One reason is that the authentication server restarted and that the provided authenication
451 /// state is no longer valid.
452 PleaseRestartAuth,
453
454 /// The request seems fine, but the session cannot be found. Either the session expired, or
455 /// was already completed. Could caused by a logic error in the client, but also by a slow
456 /// internet connection.
457 SessionGone,
458
459 /// The request seems fine, but the Yivi server is gone, perhaps because it timed out.
460 /// Also returned when the authentication server deems the yivi server stale, see [`YiviReleaseNextSessionReq::stale_after`].
461 YiviServerGone,
462
463 /// Trying to release a yivi server that's not there yet. You should first call the
464 /// [`YiviWaitForResultEP`] endpoint to make sure the yivi server is there.
465 TooEarly,
466}
467
468/// Path for the endpoint used by the yivi server to get the next session in a chained session.
469///
470/// Note that this endpoint does not conform to the [`EndpointDetails`] format, using, for example,
471/// the HTTP status code to convey information (`204` means no next session).
472pub const YIVI_NEXT_SESSION_PATH: &str = ".ph/yivi/next-session";
473
474/// Query parameters to the [`YIVI_NEXT_SESSION_PATH`] endpoint.
475#[derive(Serialize, Deserialize, Debug, Clone)]
476#[serde(deny_unknown_fields)]
477#[must_use]
478pub struct YiviNextSessionQuery {
479 pub state: AuthState,
480}