|
| 1 | +use ciborium::{from_reader, into_writer}; |
| 2 | +use ic_auth_types::{ByteBufB64, SignedDelegationCompact}; |
| 3 | +use serde::{Deserialize, Serialize, de::DeserializeOwned}; |
| 4 | +use std::str::FromStr; |
| 5 | + |
| 6 | +/// Represents a request for deep linking between applications. |
| 7 | +/// |
| 8 | +/// This struct is used to create deep link URLs for cross-application communication. |
| 9 | +/// It can be used for various scenarios including: |
| 10 | +/// - Authentication flows |
| 11 | +/// - Launching specific UI interfaces in another application |
| 12 | +/// - Opening form interfaces that return data to the calling application |
| 13 | +/// - Other cross-application communication needs |
| 14 | +/// |
| 15 | +/// # Type Parameters |
| 16 | +/// |
| 17 | +/// * `'a` - The lifetime of string references in the struct |
| 18 | +/// * `T` - The type of the payload, which must implement the [`Serialize`] trait |
| 19 | +/// |
| 20 | +/// # Examples |
| 21 | +/// |
| 22 | +/// ``` |
| 23 | +/// use ic_auth_verifier::deeplink::DeepLinkRequest; |
| 24 | +/// use url::Url; |
| 25 | +/// |
| 26 | +/// let request = DeepLinkRequest { |
| 27 | +/// os: "ios", |
| 28 | +/// action: "SignIn", |
| 29 | +/// next_url: Some("https://example.com/callback"), |
| 30 | +/// payload: Some("custom_data"), |
| 31 | +/// }; |
| 32 | +/// |
| 33 | +/// let endpoint = Url::parse("https://auth.example.com").unwrap(); |
| 34 | +/// let deep_link_url = request.to_url(&endpoint); |
| 35 | +/// ``` |
| 36 | +#[derive(Debug)] |
| 37 | +pub struct DeepLinkRequest<'a, T: Serialize> { |
| 38 | + pub os: &'a str, // e.g., "linux" | "windows" | "macos" | "ios" | "android" |
| 39 | + pub action: &'a str, // e.g., "SignIn" |
| 40 | + pub next_url: Option<&'a str>, // e.g., "https://anda.ai/deeplink" |
| 41 | + pub payload: Option<T>, // encode as base64url |
| 42 | +} |
| 43 | + |
| 44 | +impl<T> DeepLinkRequest<'_, T> |
| 45 | +where |
| 46 | + T: Serialize, |
| 47 | +{ |
| 48 | + /// Converts the request into a URL with query parameters and fragment. |
| 49 | + /// |
| 50 | + /// This method creates a URL by appending the request parameters as query parameters |
| 51 | + /// and the serialized payload (if any) as a URL fragment. The payload is serialized |
| 52 | + /// to CBOR format and then encoded as base64url. |
| 53 | + /// |
| 54 | + /// # Parameters |
| 55 | + /// |
| 56 | + /// * `endpoint` - The base URL to which parameters will be added |
| 57 | + /// |
| 58 | + /// # Returns |
| 59 | + /// |
| 60 | + /// A new `url::Url` instance with the request parameters and payload |
| 61 | + /// |
| 62 | + /// # Panics |
| 63 | + /// |
| 64 | + /// This method will panic if the payload serialization to CBOR fails |
| 65 | + pub fn to_url(&self, endpoint: &url::Url) -> url::Url { |
| 66 | + let mut url = endpoint.clone(); |
| 67 | + url.query_pairs_mut() |
| 68 | + .append_pair("os", self.os) |
| 69 | + .append_pair("action", self.action); |
| 70 | + |
| 71 | + if let Some(next_url) = self.next_url { |
| 72 | + url.query_pairs_mut().append_pair("next_url", next_url); |
| 73 | + } |
| 74 | + |
| 75 | + if let Some(payload) = &self.payload { |
| 76 | + let mut data = ByteBufB64(Vec::new()); |
| 77 | + into_writer(payload, &mut data.0).expect("Failed to serialize payload to CBOR"); |
| 78 | + url.set_fragment(Some(data.to_string().as_str())); |
| 79 | + } |
| 80 | + |
| 81 | + url |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +/// Represents a response from a deep link interaction between applications. |
| 86 | +/// |
| 87 | +/// This struct is used to parse and extract information from a URL received |
| 88 | +/// after a deep link interaction. It can be used in various cross-application scenarios: |
| 89 | +/// - Authentication flows |
| 90 | +/// - Returning data from a form or UI interaction in another application |
| 91 | +/// - Callback responses from any cross-application communication |
| 92 | +/// |
| 93 | +/// # Examples |
| 94 | +/// |
| 95 | +/// ``` |
| 96 | +/// use ic_auth_verifier::deeplink::DeepLinkResponse; |
| 97 | +/// use url::Url; |
| 98 | +/// |
| 99 | +/// let callback_url = Url::parse("https://example.com/callback?os=ios&action=SignIn#payload_data").unwrap(); |
| 100 | +/// let response = DeepLinkResponse::from_url(callback_url).unwrap(); |
| 101 | +/// ``` |
| 102 | +#[derive(Debug)] |
| 103 | +pub struct DeepLinkResponse { |
| 104 | + pub url: url::Url, |
| 105 | + pub os: String, |
| 106 | + pub action: String, // "SignIn" |
| 107 | + pub payload: Option<ByteBufB64>, // decode from base64url |
| 108 | +} |
| 109 | + |
| 110 | +impl DeepLinkResponse { |
| 111 | + /// Creates a new `DeepLinkResponse` from a URL. |
| 112 | + /// |
| 113 | + /// This method parses the URL to extract query parameters and fragment, |
| 114 | + /// constructing a `DeepLinkResponse` instance with the extracted information. |
| 115 | + /// |
| 116 | + /// # Parameters |
| 117 | + /// |
| 118 | + /// * `url` - The URL to parse, typically a callback URL from an application interaction |
| 119 | + /// |
| 120 | + /// # Returns |
| 121 | + /// |
| 122 | + /// A `Result` containing either the parsed `DeepLinkResponse` or an error message |
| 123 | + pub fn from_url(url: url::Url) -> Result<Self, String> { |
| 124 | + let mut query_pairs = url.query_pairs(); |
| 125 | + let payload = match url.fragment() { |
| 126 | + Some(f) => Some(ByteBufB64::from_str(f).map_err(|err| format!("{err:?}"))?), |
| 127 | + None => None, |
| 128 | + }; |
| 129 | + |
| 130 | + Ok(DeepLinkResponse { |
| 131 | + os: query_pairs |
| 132 | + .find(|(k, _)| k == "os") |
| 133 | + .map(|(_, v)| v.to_string()) |
| 134 | + .unwrap_or_default(), |
| 135 | + action: query_pairs |
| 136 | + .find(|(k, _)| k == "action") |
| 137 | + .map(|(_, v)| v.to_string()) |
| 138 | + .unwrap_or_default(), |
| 139 | + payload, |
| 140 | + url, |
| 141 | + }) |
| 142 | + } |
| 143 | + |
| 144 | + /// Extracts and deserializes the payload from the response. |
| 145 | + /// |
| 146 | + /// This method attempts to deserialize the payload (if present) from CBOR format |
| 147 | + /// into the specified type `T`. |
| 148 | + /// |
| 149 | + /// # Type Parameters |
| 150 | + /// |
| 151 | + /// * `T` - The type to deserialize the payload into, which must implement [`DeserializeOwned`] |
| 152 | + /// |
| 153 | + /// # Returns |
| 154 | + /// |
| 155 | + /// A `Result` containing either the deserialized payload or an error message |
| 156 | + pub fn get_payload<T: DeserializeOwned>(&self) -> Result<T, String> { |
| 157 | + if let Some(payload) = &self.payload { |
| 158 | + Ok(from_reader(payload.as_slice()).map_err(|err| format!("{err:?}"))?) |
| 159 | + } else { |
| 160 | + Err("Payload is missing in the deep link response".to_string()) |
| 161 | + } |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +/// Represents a SignIn request payload for authentication. |
| 166 | +/// |
| 167 | +/// This struct is used as the payload in a `DeepLinkRequest` for SignIn operations. |
| 168 | +/// It contains the session public key and the maximum time-to-live for the session. |
| 169 | +/// |
| 170 | +/// # Fields |
| 171 | +/// |
| 172 | +/// * `session_pubkey` - The public key for the session |
| 173 | +/// * `max_time_to_live` - The maximum time-to-live for the session in milliseconds |
| 174 | +#[derive(Clone, Default, Deserialize, Serialize)] |
| 175 | +pub struct SignInRequest { |
| 176 | + #[serde(rename = "s")] |
| 177 | + pub session_pubkey: ByteBufB64, |
| 178 | + #[serde(rename = "m")] |
| 179 | + pub max_time_to_live: u64, // in milliseconds |
| 180 | +} |
| 181 | + |
| 182 | +/// Represents a SignIn response payload from authentication. |
| 183 | +/// |
| 184 | +/// This struct is used as the payload in a `DeepLinkResponse` for SignIn operations. |
| 185 | +/// It contains the user's public key, delegations, authentication method, and origin. |
| 186 | +/// |
| 187 | +/// # Fields |
| 188 | +/// |
| 189 | +/// * `user_pubkey` - The user's public key |
| 190 | +/// * `delegations` - A vector of signed delegations that authorize the session |
| 191 | +/// * `authn_method` - The authentication method used (e.g., "webauthn", "passkey") |
| 192 | +/// * `origin` - The origin of the authentication request |
| 193 | +#[derive(Clone, Default, Deserialize, Serialize)] |
| 194 | +pub struct SignInResponse { |
| 195 | + #[serde(rename = "u")] |
| 196 | + pub user_pubkey: ByteBufB64, |
| 197 | + #[serde(rename = "d")] |
| 198 | + pub delegations: Vec<SignedDelegationCompact>, |
| 199 | + #[serde(rename = "a")] |
| 200 | + pub authn_method: String, |
| 201 | + #[serde(rename = "o")] |
| 202 | + pub origin: String, |
| 203 | +} |
0 commit comments