Skip to content

Commit 387f91b

Browse files
committed
feat: add deep link types
1 parent a7fb38b commit 387f91b

File tree

5 files changed

+224
-5
lines changed

5 files changed

+224
-5
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["rs/ic_auth_types", "rs/ic_auth_verifier"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.4.5"
6+
version = "0.4.6"
77
edition = "2024"
88
repository = "https://github.com/ldclabs/ic-auth"
99
keywords = ["config", "cbor", "canister", "icp", "encryption"]
@@ -32,3 +32,4 @@ ic-signature-verification = "0.2"
3232
ic-agent = "0.40"
3333
simple_asn1 = "0.6"
3434
xid = "1.1"
35+
url = "2.5"

rs/ic_auth_types/src/bytes.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use core::{
88
ops::{Deref, DerefMut},
99
str::FromStr,
1010
};
11-
use serde_bytes::{ByteArray, ByteBuf};
11+
12+
pub use serde_bytes::{self, ByteArray, ByteBuf, Bytes};
1213

1314
/// Wrapper around `Vec<u8>` to serialize and deserialize efficiently.
1415
/// If the serialization format is human readable (formats like JSON and YAML), it will be encoded in Base64URL.
@@ -453,6 +454,7 @@ mod deserialize {
453454
#[cfg(test)]
454455
mod tests {
455456
use super::*;
457+
use candid::encode_one;
456458
use serde::{Deserialize, Serialize};
457459

458460
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
@@ -492,5 +494,11 @@ mod tests {
492494
);
493495
let t1: Test = ciborium::from_reader(&data[..]).unwrap();
494496
assert_eq!(t, t1);
497+
498+
let a = encode_one(vec![1u8, 2, 3, 4]).unwrap();
499+
println!("candid: {}", const_hex::encode(&a));
500+
assert_eq!(a, encode_one(ByteBuf::from(vec![1, 2, 3, 4])).unwrap());
501+
assert_eq!(a, encode_one(ByteBufB64::from(vec![1, 2, 3, 4])).unwrap());
502+
assert_eq!(a, encode_one(ByteArrayB64::from([1, 2, 3, 4])).unwrap());
495503
}
496504
}

rs/ic_auth_verifier/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ http = { workspace = true, optional = true }
2626
base64 = { workspace = true, optional = true }
2727
const-hex = { workspace = true }
2828
ic-agent = { workspace = true, optional = true }
29+
url = { workspace = true, optional = true }
2930

3031
[dev-dependencies]
3132
ic-agent = { workspace = true }
@@ -35,6 +36,11 @@ rand = { workspace = true }
3536
[features]
3637
default = []
3738
full = ["sign"]
38-
envelope = ["dep:http", "dep:base64", "dep:ic-signature-verification"]
39+
envelope = [
40+
"dep:http",
41+
"dep:base64",
42+
"dep:ic-signature-verification",
43+
"dep:url",
44+
]
3945
# should not include `sign` feature for canister
4046
sign = ["envelope", "dep:ic-agent"]
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
}

rs/ic_auth_verifier/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
/// Lite version of `ic-crypto-standalone-sig-verifier`
2-
/// Original source: https://github.com/dfinity/ic/blob/master/rs/crypto/standalone-sig-verifier
31
mod asn1;
42

53
#[cfg(feature = "envelope")]
64
pub mod envelope;
75

6+
#[cfg(feature = "envelope")]
7+
pub mod deeplink;
8+
89
use k256::ecdsa::signature::hazmat::PrehashVerifier;
910
use sha3::Digest;
1011

0 commit comments

Comments
 (0)