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}