From 3cf634831bd465bc61e5fc8778f868f0ca5d390d Mon Sep 17 00:00:00 2001 From: "dobby-yivi-agent[bot]" <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:09:57 +0000 Subject: [PATCH] feat: expose frontendRequest block on SessionData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since irmago v0.14.0, POST /session returns a frontendRequest block carrying an authorization token (and protocol version bounds) used to drive the IRMA/Yivi frontend directly — most importantly to complete the pairing handshake, which is mandatory by default since v0.13.0. This block was previously dropped by serde. Add an optional frontend_request: Option field to SessionData and a FrontendRequest struct exposing authorization, min_protocol_version and max_protocol_version. The field is absent-tolerant (older servers) and skipped on serialization when None. Closes #6 Co-Authored-By: Claude Opus 4.8 --- src/irmaclient.rs | 117 ++++++++++++++++++++++++++ src/lib.rs | 4 +- tests/test_full_client_interaction.rs | 11 +++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/irmaclient.rs b/src/irmaclient.rs index 26c3a0f..098498c 100644 --- a/src/irmaclient.rs +++ b/src/irmaclient.rs @@ -36,6 +36,42 @@ pub struct SessionData { pub session_ptr: Qr, /// The token for further interaction with the session pub token: SessionToken, + /// Information needed to drive the IRMA/Yivi frontend directly (e.g. for + /// pairing). Present since irmago v0.14.0; `None` when the server does not + /// return a `frontendRequest` block. + #[serde( + rename = "frontendRequest", + default, + skip_serializing_if = "Option::is_none" + )] + pub frontend_request: Option, +} + +/// The `frontendRequest` block returned by irmago on session start, used to +/// communicate with the IRMA/Yivi frontend directly. +/// +/// Since pairing is mandatory by default for IRMA clients (irmago v0.13.0), +/// the [`authorization`](Self::authorization) token is required to complete the +/// pairing handshake. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct FrontendRequest { + /// Authorization token used to authenticate to the frontend endpoints. + pub authorization: String, + /// The lowest frontend protocol version the server supports, if reported. + #[serde( + rename = "minProtocolVersion", + default, + skip_serializing_if = "Option::is_none" + )] + pub min_protocol_version: Option, + /// The highest frontend protocol version the server supports, if reported. + #[serde( + rename = "maxProtocolVersion", + default, + skip_serializing_if = "Option::is_none" + )] + pub max_protocol_version: Option, } /// Token used to identify individual sessions on the server @@ -188,3 +224,84 @@ impl IrmaClientBuilder { } } } + +#[cfg(test)] +mod tests { + use crate::{FrontendRequest, SessionData}; + + #[test] + fn test_decode_session_data_with_frontend_request() { + let data = serde_json::from_str::( + r#" + { + "token": "KzxuWKwL5KGLKr4uerws", + "sessionPtr": { + "u": "https://example.com/irma/session/abc", + "irmaqr": "disclosing" + }, + "frontendRequest": { + "authorization": "O5Ld2vAr9pkz7ELzWqgM", + "minProtocolVersion": "1.0", + "maxProtocolVersion": "1.1" + } + } + "#, + ) + .unwrap(); + + assert_eq!( + data.frontend_request, + Some(FrontendRequest { + authorization: "O5Ld2vAr9pkz7ELzWqgM".into(), + min_protocol_version: Some("1.0".into()), + max_protocol_version: Some("1.1".into()), + }) + ); + + // Round-trips back to JSON without losing the frontend request. + let reparsed = + serde_json::from_str::(&serde_json::to_string(&data).unwrap()).unwrap(); + assert_eq!(reparsed.frontend_request, data.frontend_request); + } + + #[test] + fn test_decode_session_data_without_frontend_request() { + // Servers older than irmago v0.14.0 omit the frontendRequest block. + let data = serde_json::from_str::( + r#" + { + "token": "KzxuWKwL5KGLKr4uerws", + "sessionPtr": { + "u": "https://example.com/irma/session/abc", + "irmaqr": "disclosing" + } + } + "#, + ) + .unwrap(); + + assert_eq!(data.frontend_request, None); + + // The field is skipped on serialization when absent. + let json = serde_json::to_string(&data).unwrap(); + assert!(!json.contains("frontendRequest")); + } + + #[test] + fn test_decode_frontend_request_without_protocol_versions() { + // Only authorization is guaranteed to be useful; versions are optional. + let request = serde_json::from_str::( + r#"{ "authorization": "O5Ld2vAr9pkz7ELzWqgM" }"#, + ) + .unwrap(); + + assert_eq!( + request, + FrontendRequest { + authorization: "O5Ld2vAr9pkz7ELzWqgM".into(), + min_protocol_version: None, + max_protocol_version: None, + } + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2d5fe41..6100b0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,9 @@ mod sessionresult; mod util; pub use error::Error; -pub use irmaclient::{IrmaClient, IrmaClientBuilder, Qr, SessionData, SessionToken}; +pub use irmaclient::{ + FrontendRequest, IrmaClient, IrmaClientBuilder, Qr, SessionData, SessionToken, +}; pub use sessionrequest::{ AttributeRequest, ConDisCon, Credential, CredentialBuilder, DisclosureRequestBuilder, ExtendedIrmaRequest, IrmaRequest, IssuanceRequestBuilder, SignatureRequestBuilder, diff --git a/tests/test_full_client_interaction.rs b/tests/test_full_client_interaction.rs index 3f93f5e..9c8a438 100644 --- a/tests/test_full_client_interaction.rs +++ b/tests/test_full_client_interaction.rs @@ -69,6 +69,17 @@ fn test_full_client_interaction() { .await .expect("Failed to start session"); + // Since irmago v0.14.0 the server returns a frontendRequest block + // with an authorization token; make sure we surface it. + let frontend_request = session + .frontend_request + .as_ref() + .expect("Expected a frontend_request on session start"); + assert!( + !frontend_request.authorization.is_empty(), + "frontend_request.authorization should not be empty" + ); + let status = client .status(&session.token) .await