use url::Url;
#[allow(deprecated, reason = "Internal use of deprecated public API")]
use crate::AuthToken;
use crate::actors::auth::cookie::approval::CookieApproval;
use crate::actors::auth::cookie::builder::CookieAuthFlowBuilder;
use crate::actors::auth::cookie::credential::CookieCredential;
use crate::actors::auth::deep_links::DeepLink;
use crate::actors::auth::kind::AuthFlowKind;
use crate::actors::auth::relay::auth_relay_listener::AuthRelayListener;
use crate::errors::Result;
use crate::{Capabilities, PubkyHttpClient, PubkySession};
#[deprecated(
note = "Use PubkyGrantAuthFlow instead. Cookie-backed sessions are being phased out in favor of grant-backed, self-refreshing sessions."
)]
#[derive(Debug)]
pub struct PubkyCookieAuthFlow {
relay_listener: AuthRelayListener,
client: PubkyHttpClient,
auth_url: DeepLink,
}
#[allow(deprecated, reason = "Internal use of deprecated public API")]
impl PubkyCookieAuthFlow {
pub(crate) fn new(
relay_listener: AuthRelayListener,
client: PubkyHttpClient,
auth_url: DeepLink,
) -> Self {
Self {
relay_listener,
client,
auth_url,
}
}
pub fn start(caps: &Capabilities, auth_kind: AuthFlowKind) -> Result<Self> {
CookieAuthFlowBuilder::new(caps.clone(), auth_kind).start()
}
#[must_use]
pub fn builder(caps: &Capabilities, auth_kind: AuthFlowKind) -> CookieAuthFlowBuilder {
CookieAuthFlowBuilder::new(caps.clone(), auth_kind)
}
#[must_use]
pub fn authorization_url(&self) -> Url {
self.auth_url.clone().into()
}
pub async fn await_approval(self) -> Result<PubkySession> {
let client = self.client.clone();
let credential = self.await_credential().await?;
Ok(PubkySession::from_cookie_credential(client, credential))
}
pub async fn await_credential(self) -> Result<CookieCredential> {
let homeserver = self.target_homeserver();
let Self {
relay_listener,
client,
..
} = self;
let approval = Self::await_decoded_approval(relay_listener).await?;
CookieCredential::from_auth_token(&approval.0, &client, homeserver).await
}
pub async fn await_token(self) -> Result<AuthToken> {
let approval = Self::await_decoded_approval(self.relay_listener).await?;
Ok(approval.0)
}
pub async fn try_poll_once(&self) -> Result<Option<PubkySession>> {
let Some(credential) = self.try_poll_credential_once().await? else {
return Ok(None);
};
Ok(Some(PubkySession::from_cookie_credential(
self.client.clone(),
credential,
)))
}
pub async fn try_poll_credential_once(&self) -> Result<Option<CookieCredential>> {
let Some(approval) = self.try_decoded_approval()? else {
return Ok(None);
};
Ok(Some(
CookieCredential::from_auth_token(&approval.0, &self.client, self.target_homeserver())
.await?,
))
}
#[must_use]
pub fn try_token(&self) -> Option<Result<AuthToken>> {
match self.try_decoded_approval() {
Ok(Some(approval)) => Some(Ok(approval.0)),
Ok(None) => None,
Err(error) => Some(Err(error)),
}
}
fn target_homeserver(&self) -> Option<crate::PublicKey> {
match &self.auth_url {
DeepLink::Signup(link) => Some(link.params().homeserver.clone()),
_ => None,
}
}
async fn await_decoded_approval(relay_listener: AuthRelayListener) -> Result<CookieApproval> {
let message = relay_listener.await_message().await?;
CookieApproval::decode(&message)
}
fn try_decoded_approval(&self) -> Result<Option<CookieApproval>> {
let Some(message) = self.relay_listener.try_message() else {
return Ok(None);
};
Ok(Some(CookieApproval::decode(&message?)?))
}
}
#[cfg(test)]
#[allow(
deprecated,
reason = "Tests exercise the deprecated cookie flow on purpose"
)]
mod tests {
use super::*;
use crate::actors::auth::relay::http_relay_inbox_channel::EncryptedHttpRelayInboxChannel;
use crate::{Keypair, Pubky, deep_links::XCallbackParams};
use std::str::FromStr;
async fn build_flow(auth_kind: AuthFlowKind) -> PubkyCookieAuthFlow {
let relay = http_relay::HttpRelay::builder()
.http_port(0)
.run()
.await
.unwrap();
let inbox_base = relay.local_url().join("inbox").unwrap();
PubkyCookieAuthFlow::builder(&Capabilities::default(), auth_kind)
.client(PubkyHttpClient::new().unwrap())
.relay(inbox_base)
.start()
.unwrap()
}
async fn assert_resume_reconnects(auth_kind: AuthFlowKind) {
let relay = http_relay::HttpRelay::builder()
.http_port(0)
.run()
.await
.unwrap();
let inbox_base = relay.local_url().join("inbox").unwrap();
let client = PubkyHttpClient::new().unwrap();
let pubky = Pubky::with_client(client.clone());
let caps = Capabilities::default();
let x_callback = XCallbackParams {
x_success: Some("bitkit://auth/success?nonce=resume-cookie".into()),
..XCallbackParams::default()
};
let flow = PubkyCookieAuthFlow::builder(&caps, auth_kind)
.client(client.clone())
.relay(inbox_base)
.x_callback(x_callback.clone())
.start()
.unwrap();
let auth_url_str = flow.authorization_url().as_str().to_string();
let deep_link = DeepLink::from_str(&auth_url_str).unwrap();
assert_eq!(deep_link.x_callback(), &x_callback);
let secret = match &deep_link {
DeepLink::Signin(s) => s.params().secret,
DeepLink::Signup(s) => s.params().secret,
_ => panic!("Expected signin or signup deep link"),
};
drop(flow);
let keypair = Keypair::random();
let token = AuthToken::sign(&keypair, caps);
let token_bytes = token.serialize();
let encrypted_channel =
EncryptedHttpRelayInboxChannel::new(relay.local_url().join("inbox").unwrap(), secret)
.unwrap();
encrypted_channel
.produce(&client, &token_bytes)
.await
.unwrap();
let resumed = pubky.resume_cookie_auth_flow(&auth_url_str).unwrap();
assert_eq!(
resumed.authorization_url().as_str(),
auth_url_str,
"resumed flow produces the same authorization URL"
);
assert_eq!(
DeepLink::from_str(resumed.authorization_url().as_str())
.unwrap()
.x_callback(),
&x_callback,
"resumed flow preserves x-callback metadata"
);
let received_token = resumed.await_token().await.unwrap();
assert_eq!(
received_token, token,
"resumed flow retrieves the original token"
);
}
#[tokio::test]
async fn resume_signin_reconnects_to_same_channel() {
assert_resume_reconnects(AuthFlowKind::signin()).await;
}
#[tokio::test]
async fn resume_signup_reconnects_to_same_channel() {
let homeserver = Keypair::random().public_key();
let signup_token = Some("test-signup-token".to_string());
assert_resume_reconnects(AuthFlowKind::signup(homeserver, signup_token)).await;
}
#[test]
fn resume_rejects_invalid_url() {
let client = PubkyHttpClient::new().unwrap();
let pubky = Pubky::with_client(client);
let result = pubky.resume_cookie_auth_flow("https://not-a-pubkyauth-url.com");
assert!(result.is_err(), "non-pubkyauth URL should fail to resume");
}
#[test]
fn resume_rejects_seed_export_url() {
let client = PubkyHttpClient::new().unwrap();
let pubky = Pubky::with_client(client);
let url = "pubkyauth://secret_export?secret=kqnceEMgrNQM_xi06oQXjA3cJHX_RQmw1BY6JE1bse8";
let result = pubky.resume_cookie_auth_flow(url);
assert!(result.is_err(), "seed export URL should fail to resume");
}
#[test]
fn resume_rejects_direct_signup_url() {
let client = PubkyHttpClient::new().unwrap();
let pubky = Pubky::with_client(client);
let url =
"pubkyauth://direct_signup?hs=5jsjx1o6fzu6aeeo697r3i5rx15zq41kikcye8wtwdqm4nb4tryo";
let result = pubky.resume_cookie_auth_flow(url);
assert!(result.is_err(), "direct signup URL should fail to resume");
}
#[tokio::test]
async fn signup_flow_binds_cookie_to_deep_link_homeserver() {
let homeserver = Keypair::random().public_key();
let flow = build_flow(AuthFlowKind::signup(
homeserver.clone(),
Some("token".to_string()),
))
.await;
assert_eq!(
flow.target_homeserver(),
Some(homeserver),
"signup deep link binds the cookie to its homeserver"
);
}
#[tokio::test]
async fn signin_flow_leaves_cookie_unbound() {
let flow = build_flow(AuthFlowKind::signin()).await;
assert_eq!(
flow.target_homeserver(),
None,
"signin deep link names no homeserver; the cookie binds on revalidate"
);
}
}