From 63997511471a600d8bd5522c7c084f1cc1d84fff Mon Sep 17 00:00:00 2001 From: "dobby-yivi-agent[bot]" <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:51:56 +0000 Subject: [PATCH] feat: support chained sessions (nextSession) on ExtendedIrmaRequest Closes #9. Since irmago v0.10.0 a requestor request can declare a `nextSession` block pointing at a follow-up requestor URL, and the server links the two sessions so the second runs immediately after the first succeeds. - Add `NextSessionData { url: String }`, mirroring irmago's `RequestorBaseRequest.NextSession` struct. Confirmed against the irmago v0.19.2 `requests.go`: the JSON field is `nextSession` and the only sub-field is `url` (the `authmethod`/`key` fields floated in the issue do not exist upstream, so they are intentionally omitted). - Add `next_session: Option` to `ExtendedIrmaRequest`, serialized as `nextSession` and omitted when `None`. - Add `ExtendedIrmaRequest::new` and a chainable `next_session(url)` builder method; export `NextSessionData`. - Document chained sessions in the README, noting the recommended minimum irmago server version v0.19.0 (GHSA-pv8v-c99h-c5q4). - Add `test_next_session` covering omission, serialization and roundtrip. Co-Authored-By: Claude Opus 4.8 --- README.md | 6 ++++ src/lib.rs | 3 +- src/sessionrequest.rs | 83 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 497e5b1..a2e8744 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,9 @@ This crate provides a rust wrapper around the irma server rest API. It can be used to interface with an irma server for disclosure, signatures and issuance. Although its concept is inspired by the original irma crate (https://github.com/Wassasin/irmars) it was constructed from the ground up to provide a consistent interface with support for all of the major session types. For documentation on the IRMA ecosystem, see [the IRMA documentation](https://irma.app/docs) + +## Chained sessions + +`ExtendedIrmaRequest` supports [chained (next) sessions](https://irma.app/docs/chained-sessions): set a follow-up requestor URL with `.next_session(url)` (serialized as `nextSession`), and the server starts the linked session immediately after the current one succeeds. + +When using chained sessions in production, run an irmago server of **at least v0.19.0**. That release ships [GHSA-pv8v-c99h-c5q4](https://github.com/privacybydesign/irmago/security/advisories/GHSA-pv8v-c99h-c5q4), which tightens permission handling for next-session requests. diff --git a/src/lib.rs b/src/lib.rs index 6100b0d..0e5bfed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,8 @@ pub use irmaclient::{ }; pub use sessionrequest::{ AttributeRequest, ConDisCon, Credential, CredentialBuilder, DisclosureRequestBuilder, - ExtendedIrmaRequest, IrmaRequest, IssuanceRequestBuilder, SignatureRequestBuilder, + ExtendedIrmaRequest, IrmaRequest, IssuanceRequestBuilder, NextSessionData, + SignatureRequestBuilder, }; pub use sessionresult::{ AttributeStatus, DisclosedAttribute, ProofStatus, SessionResult, SessionStatus, SessionType, diff --git a/src/sessionrequest.rs b/src/sessionrequest.rs index bc82a48..1905bac 100644 --- a/src/sessionrequest.rs +++ b/src/sessionrequest.rs @@ -438,6 +438,24 @@ impl IssuanceRequestBuilder { } } +/// Data describing a chained ("next") session to start after the current one +/// succeeds. Mirrors the `NextSessionData` struct on irmago's `RequestorBaseRequest` +/// (introduced in irmago v0.10.0); as of irmago v0.19.2 it carries a single `url` +/// field. See . +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub struct NextSessionData { + /// URL from which to get the next session request once this session succeeds. + pub url: String, +} + +impl NextSessionData { + /// Create the data for a chained session pointing at the given requestor URL. + pub fn new(url: String) -> NextSessionData { + NextSessionData { url } + } +} + /// An IRMA request extended with extra information for the server on how to execute it. /// (Note: this interface is unstable, and might change significantly in the future) #[derive(Serialize, Deserialize, Debug, Clone)] @@ -452,10 +470,38 @@ pub struct ExtendedIrmaRequest { /// URL on which to recieve updates as the session status changes #[serde(rename = "callbackUrl", skip_serializing_if = "Option::is_none")] pub callback_url: Option, + /// A chained session to start once this session succeeds. Serialized as + /// `nextSession` to match irmago's `RequestorBaseRequest` and omitted when `None`. + /// Recommended only with irmago server v0.19.0 or newer (see GHSA-pv8v-c99h-c5q4). + #[serde(rename = "nextSession", skip_serializing_if = "Option::is_none")] + pub next_session: Option, /// Inner request pub request: IrmaRequest, } +impl ExtendedIrmaRequest { + /// Wrap an inner request without any extra server instructions. + pub fn new(request: IrmaRequest) -> ExtendedIrmaRequest { + ExtendedIrmaRequest { + validity: None, + timeout: None, + callback_url: None, + next_session: None, + request, + } + } + + /// Chain a follow-up session to be started by the server immediately after this + /// one succeeds, pointing at the given requestor URL (irmago `nextSession`). + /// + /// For production use this requires an irmago server of at least v0.19.0, which + /// ships the tightened next-session permission handling from GHSA-pv8v-c99h-c5q4. + pub fn next_session(mut self, url: String) -> ExtendedIrmaRequest { + self.next_session = Some(NextSessionData::new(url)); + self + } +} + #[cfg(test)] mod tests { use std::time::Duration; @@ -465,8 +511,8 @@ mod tests { use crate::CredentialBuilder; use super::{ - AttributeRequest, Credential, DisclosureRequestBuilder, IssuanceRequestBuilder, - SignatureRequestBuilder, TranslatedString, + AttributeRequest, Credential, DisclosureRequestBuilder, ExtendedIrmaRequest, + IssuanceRequestBuilder, NextSessionData, SignatureRequestBuilder, TranslatedString, }; #[test] @@ -774,4 +820,37 @@ mod tests { serde_json::from_str(&serde_json::to_string(&req6).unwrap()).unwrap() ); } + + #[test] + fn test_next_session() { + let inner = DisclosureRequestBuilder::new() + .add_discon(vec![vec![AttributeRequest::Simple("a.b.c.d".into())]]) + .build(); + + // Omitted entirely when not set. + let req1 = ExtendedIrmaRequest::new(inner.clone()); + assert_eq!( + "{\"request\":{\"@context\":\"https://irma.app/ld/request/disclosure/v2\",\"disclose\":[[[\"a.b.c.d\"]]]}}", + serde_json::to_string(&req1).unwrap() + ); + assert_eq!( + req1, + serde_json::from_str(&serde_json::to_string(&req1).unwrap()).unwrap() + ); + + // Serialized as `nextSession` with a single `url` sub-field, matching irmago. + let req2 = ExtendedIrmaRequest::new(inner).next_session("https://example.com/next".into()); + assert_eq!( + "{\"nextSession\":{\"url\":\"https://example.com/next\"},\"request\":{\"@context\":\"https://irma.app/ld/request/disclosure/v2\",\"disclose\":[[[\"a.b.c.d\"]]]}}", + serde_json::to_string(&req2).unwrap() + ); + assert_eq!( + req2.next_session, + Some(NextSessionData::new("https://example.com/next".into())) + ); + assert_eq!( + req2, + serde_json::from_str(&serde_json::to_string(&req2).unwrap()).unwrap() + ); + } }