use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use reqwest::Method;
use url::Url;
use pubky_common::{
auth::AuthToken,
crypto::{encrypt, hash},
};
use crate::{
Capabilities, cross_log,
errors::{AuthError, Result},
util::check_http_status,
};
use super::PubkySigner;
impl PubkySigner {
#[allow(
clippy::cognitive_complexity,
reason = "Approving a flow requires a fixed sequence of validation steps kept together for clarity"
)]
pub async fn approve_auth(&self, pubkyauth_url: impl AsRef<str>) -> Result<()> {
let pubkyauth_url = Url::parse(pubkyauth_url.as_ref())?;
let (relay, client_secret) = Self::parse_relay_and_secret(&pubkyauth_url)?;
cross_log!(info, "Approving auth flow via relay {relay}");
let capabilities = Capabilities::from(&pubkyauth_url);
cross_log!(
info,
"Signing capabilities {:?} for auth approval",
capabilities
);
let encrypted_token = self.build_encrypted_token(capabilities, &client_secret);
let callback_url = Self::derive_callback_url(&relay, &client_secret)?;
cross_log!(
info,
"Posting encrypted auth token to relay channel {}",
callback_url
);
let response = self
.client
.cross_request(Method::POST, callback_url)
.await?
.body(encrypted_token)
.send()
.await?;
check_http_status(response).await?;
cross_log!(info, "Auth token delivered successfully");
Ok(())
}
fn parse_relay_and_secret(pubkyauth_url: &Url) -> Result<(Url, [u8; 32])> {
let mut relay_param: Option<String> = None;
let mut secret_param: Option<String> = None;
for (key, value) in pubkyauth_url.query_pairs() {
match key.as_ref() {
"relay" if relay_param.is_none() => relay_param = Some(value.into_owned()),
"secret" if secret_param.is_none() => secret_param = Some(value.into_owned()),
_ => {}
}
}
let relay_str = relay_param
.ok_or_else(|| AuthError::Validation("Missing 'relay' query parameter".to_string()))?;
let relay = Url::parse(&relay_str)?;
let secret_str = secret_param
.ok_or_else(|| AuthError::Validation("Missing 'secret' query parameter".to_string()))?;
let secret_bytes = URL_SAFE_NO_PAD
.decode(secret_str)
.map_err(|e| AuthError::Validation(format!("Invalid base64 secret: {e}")))?;
let client_secret: [u8; 32] = secret_bytes
.try_into()
.map_err(|_err| AuthError::Validation("Client secret must be 32 bytes".to_string()))?;
Ok((relay, client_secret))
}
fn build_encrypted_token(
&self,
capabilities: Capabilities,
client_secret: &[u8; 32],
) -> Vec<u8> {
let token = AuthToken::sign(&self.keypair, capabilities);
encrypt(&token.serialize(), client_secret)
}
fn derive_callback_url(relay: &Url, client_secret: &[u8; 32]) -> Result<Url> {
let mut callback_url = relay.clone();
let mut path_segments = callback_url
.path_segments_mut()
.map_err(|()| url::ParseError::RelativeUrlWithCannotBeABaseBase)?;
path_segments.pop_if_empty();
let channel_id = URL_SAFE_NO_PAD.encode(hash(client_secret).as_bytes());
path_segments.push(&channel_id);
drop(path_segments);
Ok(callback_url)
}
}