use ciborium::from_reader;
use ic_auth_types::{ByteBufB64, SignedDelegationCompact, deterministic_cbor_into_vec};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::str::FromStr;
#[derive(Debug)]
pub struct DeepLinkRequest<'a, T: Serialize> {
pub os: &'a str, pub action: &'a str, pub next_url: Option<&'a str>, pub payload: Option<T>, }
impl<T> DeepLinkRequest<'_, T>
where
T: Serialize,
{
pub fn try_to_url(&self, endpoint: &url::Url) -> Result<url::Url, String> {
let mut url = endpoint.clone();
{
let mut query = url.query_pairs_mut();
query
.append_pair("os", self.os)
.append_pair("action", self.action);
if let Some(next_url) = self.next_url {
query.append_pair("next_url", next_url);
}
}
if let Some(payload) = &self.payload {
let data = deterministic_cbor_into_vec(payload)
.map_err(|err| format!("failed to serialize payload to CBOR: {err}"))?;
let fragment = ByteBufB64(data).to_string();
url.set_fragment(Some(&fragment));
} else {
url.set_fragment(None);
}
Ok(url)
}
pub fn to_url(&self, endpoint: &url::Url) -> url::Url {
self.try_to_url(endpoint)
.expect("Failed to serialize payload to CBOR")
}
}
#[derive(Debug)]
pub struct DeepLinkResponse {
pub url: url::Url,
pub os: String,
pub action: String, pub payload: Option<ByteBufB64>, }
impl DeepLinkResponse {
pub fn from_url(url: url::Url) -> Result<Self, String> {
let payload = match url.fragment() {
Some(f) => Some(ByteBufB64::from_str(f).map_err(|err| format!("{err:?}"))?),
None => None,
};
let mut os = None;
let mut action = None;
for (key, value) in url.query_pairs() {
match key.as_ref() {
"os" if os.is_none() => os = Some(value.into_owned()),
"action" if action.is_none() => action = Some(value.into_owned()),
_ => {}
}
}
Ok(DeepLinkResponse {
os: os.unwrap_or_default(),
action: action.unwrap_or_default(),
payload,
url,
})
}
pub fn get_payload<T: DeserializeOwned>(&self) -> Result<T, String> {
if let Some(payload) = &self.payload {
Ok(from_reader(payload.as_slice()).map_err(|err| format!("{err:?}"))?)
} else {
Err("Payload is missing in the deep link response".to_string())
}
}
}
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct SignInRequest {
#[serde(rename = "s")]
pub session_pubkey: ByteBufB64,
#[serde(rename = "m")]
pub max_time_to_live: u64, }
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct SignInResponse {
#[serde(rename = "u")]
pub user_pubkey: ByteBufB64,
#[serde(rename = "d")]
pub delegations: Vec<SignedDelegationCompact>,
#[serde(rename = "a")]
pub authn_method: String,
#[serde(rename = "o")]
pub origin: String,
}
#[cfg(test)]
mod tests {
use super::*;
use url::Url;
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct Payload {
value: String,
}
#[test]
fn test_response_reads_query_params_in_any_order() {
let url = Url::parse("https://example.com/callback?action=SignIn&os=ios").unwrap();
let response = DeepLinkResponse::from_url(url).unwrap();
assert_eq!(response.os, "ios");
assert_eq!(response.action, "SignIn");
}
#[test]
fn test_request_clears_endpoint_fragment_without_payload() {
let endpoint = Url::parse("https://auth.example.com/start#stale-fragment").unwrap();
let request = DeepLinkRequest::<()> {
os: "ios",
action: "SignIn",
next_url: None,
payload: None,
};
let url = request.try_to_url(&endpoint).unwrap();
assert_eq!(url.fragment(), None);
}
#[test]
fn test_request_response_payload_roundtrip() {
let endpoint = Url::parse("https://auth.example.com/start").unwrap();
let request = DeepLinkRequest {
os: "ios",
action: "SignIn",
next_url: Some("https://example.com/callback"),
payload: Some(Payload {
value: "hello".to_string(),
}),
};
let url = request.try_to_url(&endpoint).unwrap();
let response = DeepLinkResponse::from_url(url).unwrap();
let payload: Payload = response.get_payload().unwrap();
assert_eq!(response.os, "ios");
assert_eq!(response.action, "SignIn");
assert_eq!(payload.value, "hello");
}
}